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

coniglio

v2.2.0

Published

A minimal, elegant, and robust async RabbitMQ client for Node.js

Readme

🐇 coniglio

A small, typed and resilient RabbitMQ client for Node.js.

Coniglio wraps amqplib with:

  • async-iterator consumers with bounded prefetch;
  • publisher confirms and configurable retry;
  • automatic connection, channel, subscription and topology recovery;
  • explicit ack() / nack() ownership;
  • graceful shutdown and AbortSignal support;
  • consistent JSON encoding with raw Buffer support;
  • ESM, CommonJS and first-class TypeScript types.

Installation

npm install coniglio

Coniglio supports Node.js 22 and newer.

Quick start

import coniglio from 'coniglio'

type Events = {
  'user.created': { userId: string }
  'invoice.sent': { invoiceId: string; total: number }
}

const rabbit = await coniglio<Events>('amqp://localhost')

await rabbit.configure({
  exchanges: [{ name: 'domain.events', type: 'topic', durable: true }],
  queues: [
    {
      name: 'users',
      durable: true,
      bindTo: [{ exchange: 'domain.events', routingKey: 'user.created' }],
    },
  ],
})

await rabbit.publish('domain.events', 'user.created', { userId: '42' })

for await (const message of rabbit.listen('users', {
  routingKeys: ['user.created'],
})) {
  try {
    if (message.contentIsJson) {
      await createUser(message.data.userId)
    }
    rabbit.ack(message)
  } catch (error) {
    rabbit.nack(message, true)
  }
}

Always close the client during application shutdown:

await rabbit.close()

Creating a client

const rabbit = await coniglio('amqp://localhost', {
  logger: console,
  onEvent: (event) => {
    metrics.increment(`coniglio.${event.type}`)
  },
  json: true,
  prefetch: 10,
  reconnect: {
    initialDelayMs: 1000,
    maxDelayMs: 30000,
    maxAttempts: Infinity,
  },
  publish: {
    confirmTimeoutMs: 30000,
    retry: {
      initialDelayMs: 1000,
      maxDelayMs: 30000,
      maxAttempts: Infinity,
    },
  },
  signal: applicationAbortController.signal,
  socketOptions: {
    timeout: 10000,
  },
})

All options except the URL are optional.

Logging

A logger may implement any subset of these methods:

interface Logger {
  debug?(...args: unknown[]): void
  info?(...args: unknown[]): void
  warn?(...args: unknown[]): void
  error?(...args: unknown[]): void
}

Pino and console can be passed directly.

Consuming

listen() creates a dedicated RabbitMQ channel and returns an async generator:

for await (const message of rabbit.listen('users', {
  prefetch: 20,
  json: true,
  signal: workerAbortController.signal,
})) {
  if (message.contentIsJson) {
    console.log(message.data)
  } else {
    console.warn('Non-JSON payload:', message.content)
  }

  rabbit.ack(message)
}

Breaking out of the loop, calling iterator.return(), aborting its signal, or closing the client cancels the RabbitMQ consumer and closes its channel.

Routing-key assertions

routingKeys narrows the TypeScript result and asserts the queue contract:

for await (const message of rabbit.listen('users', {
  routingKeys: ['user.created'],
})) {
  if (message.contentIsJson) {
    message.data.userId
  }
}

RabbitMQ routing must still be configured through exchanges and queue bindings. If the queue delivers a key outside routingKeys, Coniglio requeues the message, stops that iterator and throws UnexpectedRoutingKeyError. It never silently discards an unexpected message.

Acknowledgements

rabbit.ack(message)
rabbit.nack(message)
rabbit.nack(message, true) // requeue

Acknowledgements are sent through the exact channel that delivered the message. If that channel was lost during processing, ack() and nack() throw ConiglioMessageStateError; RabbitMQ requeues the unacknowledged delivery when the old channel closes.

Publishing

Non-Buffer values are always encoded using JSON.stringify():

await rabbit.publish('domain.events', 'user.created', {
  userId: '42',
})

// The wire body is the valid JSON string: "hello"
await rabbit.publish('', 'text', 'hello')

Buffer payloads are sent unchanged with an application/octet-stream content type:

await rabbit.publish('', 'binary', Buffer.from([1, 2, 3]))

Standard amqplib publish options and Coniglio controls share the final argument:

await rabbit.publish(
  'domain.events',
  'invoice.sent',
  { invoiceId: 'inv-1', total: 120 },
  {
    persistent: true,
    priority: 5,
    confirmTimeoutMs: 5000,
    retry: {
      initialDelayMs: 100,
      maxDelayMs: 5000,
      maxAttempts: 8,
    },
    signal: requestAbortController.signal,
  },
)

Use retry: false for one attempt.

Publishing provides at-least-once, not exactly-once, semantics. If RabbitMQ accepts a message but its confirm is lost with the connection, a retry can publish a duplicate. Consumers should be idempotent when duplicates matter.

Serialization errors are returned immediately and are never retried.

Set mandatory: true when the caller must know that a message reached at least one queue. RabbitMQ returns unroutable mandatory messages and Coniglio rejects the publish immediately with ConiglioUnroutableError, without waiting for the publisher confirm. These definitive failures are never retried. The error includes the exchange, routing key, message ID and RabbitMQ reply code and text.

Coniglio generates a message ID when a mandatory publish does not provide one. Concurrent returns are correlated independently from that application-visible ID, so the same message ID can safely be reused by overlapping attempts. Coniglio adds a reserved x-coniglio-publish-token header to mandatory publishes for this correlation; callers should not assign meaning to it.

A publisher-confirm timeout has an unknown outcome: RabbitMQ might have accepted the message even though the confirmation did not arrive in time. Coniglio retries ConiglioPublishTimeoutError according to the configured publish retry policy. With retry: false, or after the retry budget is exhausted, the typed timeout error is returned directly to the caller.

Topology

await rabbit.configure({
  exchanges: [
    {
      name: 'domain.events',
      type: 'topic',
      durable: true,
    },
    {
      name: 'delayed.events',
      type: 'x-delayed-message',
      durable: true,
      arguments: {
        'x-delayed-type': 'topic',
      },
    },
  ],
  queues: [
    {
      name: 'invoices',
      durable: true,
      deadLetterExchange: 'dead-letters',
      messageTtl: 60000,
      maxLength: 10000,
      bindTo: [
        {
          exchange: 'domain.events',
          routingKey: 'invoice.*',
        },
      ],
    },
  ],
})

Successful calls to configure() are remembered. After reconnecting, Coniglio redeclares exchanges, queues and bindings before recreating active consumers. The x-delayed-message exchange type requires the RabbitMQ delayed-message plugin on the broker.

Recovery model

Coniglio uses exponential backoff with jitter.

  • A connection or publisher-channel close rebuilds the full transport.
  • Each listener has an isolated consumer channel.
  • A consumer-channel close rebuilds the complete transport.
  • Stored topology is applied before subscriptions restart.
  • Buffered deliveries from a closed channel are left for RabbitMQ to requeue.
  • Pending listeners survive reconnect unless their retry budget is exhausted.
  • close() and abort signals stop sleeps, retries and consumers.

Set finite maxAttempts values when the caller must regain control after a bounded retry window. The defaults remain infinite for backward compatibility.

Lifecycle and observability

The current lifecycle is available without parsing logs:

rabbit.state
// 'idle' | 'connecting' | 'ready' | 'reconnecting'
// | 'disconnected' | 'closing' | 'closed'

Use onEvent for metrics and tracing:

const rabbit = await coniglio('amqp://localhost', {
  onEvent(event) {
    switch (event.type) {
      case 'connection-retry':
        metrics.increment('rabbitmq.connection.retry')
        metrics.observe('rabbitmq.connection.backoff', event.delayMs)
        break
      case 'consumer-ready':
        metrics.increment('rabbitmq.consumer.ready', {
          queue: event.queue,
        })
        break
      case 'publish-confirmed':
        metrics.increment('rabbitmq.publish.confirmed', {
          routingKey: event.routingKey,
        })
        break
    }
  },
})

Event-hook failures are isolated and never affect delivery.

Multiple connections

Each call creates an isolated client:

const production = await coniglio('amqp://production')
const qa = await coniglio('amqp://qa')

await production.publish('events', 'ready', { environment: 'production' })
await qa.publish('events', 'ready', { environment: 'qa' })

await Promise.all([production.close(), qa.close()])

Public types and errors

All public types are exported from the package root:

import coniglio, {
  ConiglioClosedError,
  ConiglioMessageStateError,
  ConiglioPublishError,
  ConiglioPublishTimeoutError,
  ConiglioUnroutableError,
  UnexpectedRoutingKeyError,
  type ConfigureOptions,
  type ConiglioInstance,
  type ConiglioEvent,
  type ConiglioLifecycleState,
  type ConiglioOptions,
  type Message,
  type PublishOptions,
} from 'coniglio'

Both ESM imports and CommonJS require('coniglio') are supported.

Development

npm ci
npm run check
npm run test:integration
npm pack --dry-run

The integration suite expects RabbitMQ at amqp://localhost. CI runs the suite against a RabbitMQ service and checks Node.js 22, 24 and 26.

License

MIT