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

velink

v1.1.0

Published

A production-grade Lavalink v4 client with circuit breakers, smart load balancing, and exceptional developer experience.

Downloads

271

Readme

Velink


Table of Contents


Features

  • Lavalink v4 Protocol -- Full support for the latest Lavalink v4 WebSocket and REST API
  • Multiple Lavalink Nodes -- Connect to and manage multiple Lavalink nodes simultaneously
  • Smart Load Balancing -- 4 strategies: round-robin, weighted round-robin, least players, least load
  • Circuit Breaker -- Automatic failure detection and recovery to prevent cascading failures
  • Health Monitoring -- Periodic liveness/readiness checks with configurable thresholds
  • Stale Player Cleanup -- Automatic cleanup of disconnected/idle players to prevent memory leaks
  • Resilient Reconnection -- Exponential backoff with jitter for WebSocket reconnection
  • Request Deduplication -- REST client coalesces identical concurrent requests
  • Lazy Track Resolution -- Unresolved tracks save memory until playback time
  • Optimized Deque -- O(1) queue operations via doubly-linked list (no array shift costs)
  • 22 Audio Filter Presets -- bassboost, nightcore, vaporwave, 8D, karaoke, and more
  • Full Type Safety -- Written in TypeScript with strict mode and Zod schema validation
  • Comprehensive Error Hierarchy -- Structured errors with codes for programmatic handling

Installation

npm install velink

Peer dependency (optional):

npm install discord.js  # Only if using the DiscordJSConnector

Requirements:

  • Node.js >= 18.0.0
  • Lavalink server v4.x

Quick Start

import { Client, GatewayIntentBits } from "discord.js";
import { Velink } from "velink";
import { DiscordJSConnector } from "velink/connectors/discordjs";

// 1. Create Discord client
const discord = new Client({
  intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildVoiceStates,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.MessageContent,
  ],
});

// 2. Create Velink client
const velink = new Velink({
  nodes: [
    {
      host: "localhost",
      port: 2333,
      auth: "your-lavalink-password",
      secure: false,
      priority: 0,
    },
  ],
  autoResume: true,
  loadBalancing: {
    strategy: "weightedRoundRobin",
  },
});

// 3. Connect Discord and Velink
const connector = new DiscordJSConnector(discord, velink);

discord.on("ready", async () => {
  console.log(`Logged in as ${discord.user?.tag}`);
  await connector.init();
});

// 4. Play music on command
discord.on("messageCreate", async (message) => {
  if (message.content.startsWith("!play ")) {
    const query = message.content.slice(6);
    const voiceChannel = message.member?.voice.channel;
    if (!voiceChannel) {
      return message.reply("You must be in a voice channel!");
    }

    // Search for tracks
    const result = await velink.search(query, "youtube");
    if (result.tracks.length === 0) {
      return message.reply("No results found.");
    }

    // Create player and connect
    const player = await velink.createPlayer({
      guildId: message.guildId!,
      voiceChannelId: voiceChannel.id,
      textChannelId: message.channelId,
      selfDeaf: true,
    });

    await player.connect();

    // Add to queue and play
    player.queue.addMany(result.tracks.slice(1));
    await player.play(result.tracks[0]!);

    message.reply(`Now playing: **${result.tracks[0]!.title}**`);
  }
});

// 5. Handle events
velink.on("trackStart", (player, track) => {
  console.log(`Started playing: ${track.title} by ${track.author}`);
});

velink.on("trackEnd", (player, track, reason) => {
  console.log(`Finished: ${track.title} (${reason})`);
});

velink.on("queueEnd", (player) => {
  console.log("Queue finished");
});

discord.login("YOUR_BOT_TOKEN");

Core Concepts

Architecture

Velink (client)
  |-- Node[] (Lavalink connections)
  |     |-- Player[] (guild players)
  |     |-- RestClient (HTTP API)
  |     |-- CircuitBreaker (failure protection)
  |     |-- HealthMonitor (health probes)
  |
  |-- LoadBalancer (node selection)
  |-- StalePlayerCleanup (memory management)
  |-- Queue (per-player track queue)
  |-- Track (track representation)

Lifecycle

  1. Create Velink with node configuration
  2. Initialize with bot user ID
  3. Create Player for a guild (auto-selects best node)
  4. Connect to voice channel
  5. Search for tracks via REST API
  6. Play track (sends WebSocket op to Lavalink)
  7. Events flow back via WebSocket
  8. Cleanup on disconnect or timeout

API Reference

Velink Client

new Velink(options)

Creates the main Velink client.

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | nodes | NodeOptions[] | Yes | Lavalink node configurations | | userId | string | No | Discord bot user ID | | shardCount | number | No | Number of shards | | autoResume | boolean | No | Enable automatic session resume (default: false) | | autoResumeTimeout | number | No | Resume timeout in ms (default: 60000) | | playerCleanupInterval | number | No | Stale player check interval (default: 300000) | | playerCleanupThreshold | number | No | Player idle timeout (default: 300000) | | circuitBreaker | object | No | Circuit breaker configuration | | loadBalancing | object | No | Load balancing configuration |

Returns: Velink instance

velink.init(userId)

Initialize and connect to all configured nodes.

| Parameter | Type | Description | |-----------|------|-------------| | userId | string | Discord bot user ID |

Returns: Promise<void>

Throws: ClientDestroyedError, ValidationError

velink.createPlayer(options)

Create a new player for a guild.

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | guildId | string | Yes | Discord guild ID | | voiceChannelId | string | No | Voice channel ID | | textChannelId | string | No | Text channel ID for feedback | | selfDeaf | boolean | No | Join deafened (default: true) | | selfMute | boolean | No | Join muted (default: false) | | volume | number | No | Initial volume 0-1000 (default: 100) | | nodeId | string | No | Specific node to use |

Returns: Promise<Player>

Throws: ClientDestroyedError, NoAvailableNodesError, NodeNotFoundError

velink.search(query, source?)

Search for tracks via Lavalink REST API.

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | query | string | Yes | Search query | | source | TrackSource | No | Source prefix (e.g., "youtube", "soundcloud") |

Returns: Promise<{ loadType, tracks, playlist, exception }>

Throws: NoAvailableNodesError

velink.destroy()

Gracefully shut down the client.

Returns: Promise<void>


Node

node.connect(userId)

Establish WebSocket connection to Lavalink.

Returns: Promise<void>

Throws: NodeDestroyedError, NodeConnectionError

node.destroy()

Destroy the node and all its players.

node.loadTracks(query)

Load tracks via REST API.

Returns: Promise<TrackLoadResult>

node.updatePlayer(guildId, payload)

Update a player on this node.

Returns: Promise<Record<string, unknown>>


Player

player.connect(channelId?)

Connect to a voice channel.

Returns: Promise<void>

Throws: PlayerDestroyedError, VoiceChannelRequiredError

player.play(track, options?)

Play a track.

| Parameter | Type | Required | Description | |-----------|------|----------|-------------| | track | Track | Yes | Track to play | | options.noReplace | boolean | No | Don't replace current track | | options.startTime | number | No | Start position in ms | | options.endTime | number | No | End position in ms | | options.volume | number | No | Volume 0-1000 |

Returns: Promise<void>

Throws: PlayerDestroyedError

player.pause()

Pause playback.

Returns: Promise<void>

player.resume()

Resume playback.

Returns: Promise<void>

player.stop()

Stop playback and clear current track.

Returns: Promise<void>

player.seek(positionMs)

Seek to position in current track.

| Parameter | Type | Description | |-----------|------|-------------| | positionMs | number | Position in milliseconds |

Returns: Promise<void>

player.setVolume(volume)

Set volume (0-1000).

Returns: Promise<void>

player.setFilters(filters)

Apply Lavalink audio filters.

Returns: Promise<void>

player.skip()

Skip to next track in queue.

Returns: Promise<Track | null>

player.previous()

Go back to previous track.

Returns: Promise<Track | null>

player.setRepeatMode(mode)

Set repeat mode.

| Value | Description | |-------|-------------| | "off" | No repeat | | "track" | Repeat current track | | "queue" | Repeat entire queue |

player.destroy()

Destroy player and clean up.

Returns: Promise<void>


Queue

queue.add(track)

Add track to end. Returns boolean (false if full).

queue.addMany(tracks)

Add multiple tracks. Returns count added.

queue.insertNext(track)

Insert track at front (plays next).

queue.next()

Get next track (handles repeat modes).

Returns: Track | null

queue.previous()

Get previous track from history.

Returns: Track | null

queue.skipTo(index)

Skip to track at index.

Returns: Track | null

queue.shuffle()

Shuffle all tracks using Fisher-Yates.

queue.clear()

Remove all tracks and history.

queue.remove(predicate)

Remove first matching track.

Returns: Track | null

queue.duration

Total duration of all tracks in ms (cached).


Track

new Track(data, logger)

Create from Lavalink track data or unresolved metadata.

track.isResolved

Whether the track has full Lavalink data.

track.resolve(resolver)

Resolve an unresolved track.

| Parameter | Type | Description | |-----------|------|-------------| | resolver | (query: string) => Promise<LavalinkTrack> | Resolution function |

Returns: Promise<void>

Throws: TrackResolveError

track.toJSON()

Serialize to plain object.

Track.fromUnresolved(title, author, source, logger, extra?)

Create unresolved track from metadata.


Filters

new FilterBuilder()

Fluent API for building audio filters.

const filters = new FilterBuilder()
  .preset("bassboost")
  .timescale({ speed: 1.1, pitch: 1.05 })
  .volume(1.2)
  .commit();

await player.setFilters(filters);

Available Methods:

| Method | Parameters | Description | |--------|-----------|-------------| | .volume(value) | 0-5 | Volume multiplier | | .equalizer(bands) | LavalinkEqualizerBand[] | EQ bands | | .equalizerFromGains(gains) | number[] (15 values) | EQ from gain array | | .karaoke(options?) | { level?, monoLevel?, filterBand?, filterWidth? } | Vocal suppression | | .timescale(options) | { speed?, pitch?, rate? } | Speed/pitch/rate | | .tremolo(options) | { frequency?, depth? } | Tremolo effect | | .vibrato(options) | { frequency?, depth? } | Vibrato effect | | .rotation(hz) | number | 8D audio rotation | | .distortion(options) | { sinOffset?, sinScale?, ... } | Distortion | | .channelMix(options) | { leftToLeft?, leftToRight?, ... } | Stereo panning | | .lowPass(smoothing) | number | Low pass filter | | .preset(name) | FilterPresetName | Apply named preset | | .reset() | | Clear all filters | | .remove(filter) | keyof LavalinkFilter | Remove specific filter | | .build() | | Get filter object | | .commit() | | Get and mark as committed |

22 Built-in Presets: bassboost, bassboostLow, bassboostMedium, bassboostHigh, bassboostExtreme, nightcore, superNightcore, vaporwave, pop, soft, treble, trebleBass, eightD, vibrato, tremolo, karaoke, lowPass, dim, earrape, phaser, chinatown, party


Error Handling

Velink uses a comprehensive error hierarchy with machine-readable codes:

import {
  VelinkError,
  NodeNotFoundError,
  NodeConnectionError,
  CircuitBreakerOpenError,
  PlayerDestroyedError,
  NoAvailableNodesError,
} from "velink";

try {
  const player = await velink.createPlayer({ guildId: "123" });
} catch (error) {
  if (error instanceof NoAvailableNodesError) {
    console.error("All Lavalink nodes are down!");
    // Send alert to monitoring
  } else if (error instanceof NodeNotFoundError) {
    console.error(`Node not found: ${error.context.nodeId}`);
  }

  // All errors have codes
  console.log(error.code); // "NO_AVAILABLE_NODES"
  console.log(error.toDisplayMessage()); // "[NO_AVAILABLE_NODES] ..."
  console.log(error.toJSON()); // { name, message, code, context, timestamp }
}

Error Codes

| Code | Description | |------|-------------| | NODE_ERROR | Generic node error | | NODE_NOT_FOUND | Specified node doesn't exist | | NODE_CONNECTION_ERROR | WebSocket connection failed | | NODE_DISCONNECTED | Node disconnected during operation | | NODE_DESTROYED | Node has been destroyed | | CIRCUIT_BREAKER_OPEN | Circuit breaker is rejecting requests | | TRACK_LOAD_ERROR | Failed to load track from Lavalink | | TRACK_RESOLVE_ERROR | Lazy track resolution failed | | NO_TRACKS_FOUND | Search returned no results | | PLAYER_CONNECTION_ERROR | Voice connection failed | | PLAYER_NOT_FOUND | Player doesn't exist for guild | | PLAYER_DESTROYED | Player has been destroyed | | VOICE_CHANNEL_REQUIRED | Voice channel ID not provided | | REST_ERROR | HTTP request failed | | REST_TIMEOUT | HTTP request timed out | | VALIDATION_ERROR | Options validation failed | | CLIENT_DESTROYED | Velink client is destroyed | | NO_AVAILABLE_NODES | All nodes are unavailable |


Load Balancing

4 strategies for distributing players across nodes:

| Strategy | Description | Use Case | |----------|-------------|----------| | roundRobin | Cycles evenly through nodes | Balanced distribution | | weightedRoundRobin | Probability-based by node priority | Prioritize powerful nodes | | leastPlayers | Picks node with fewest players | Even player distribution | | leastLoad | Picks node with lowest CPU+memory | Resource optimization |

const velink = new Velink({
  nodes: [
    { host: "node1", port: 2333, auth: "pw", priority: 10 },
    { host: "node2", port: 2333, auth: "pw", priority: 5 },
  ],
  loadBalancing: {
    strategy: "weightedRoundRobin",
    healthCheckInterval: 30000,
    readinessThreshold: {
      maxCpuLoad: 0.8,
      maxMemoryUsage: 0.9,
    },
  },
});

Circuit Breaker

Prevents cascading failures by temporarily rejecting requests to failing nodes.

State Machine:

CLOSED (normal) --failures>=threshold--> OPEN (rejecting)
   ^                                        |
   |--successes>=halfOpenMaxCalls-- HALF_OPEN (testing)
             ^------------------resetTimeout--|
// Configure in Velink options
const velink = new Velink({
  nodes: [...],
  circuitBreaker: {
    failureThreshold: 5,   // Open after 5 consecutive failures
    resetTimeout: 30000,   // Try recovery after 30 seconds
    halfOpenMaxCalls: 3,   // Allow 3 test requests in half-open
  },
});

Health Monitoring

Periodic HTTP probes to each Lavalink node:

  • Liveness (every 15s): Quick /info check to confirm reachability
  • Readiness (every 30s): Full /stats evaluation against CPU/memory thresholds

Nodes exceeding thresholds are excluded from load balancing until they recover.


Discord.js Integration

The DiscordJSConnector bridges Discord.js and Velink:

import { DiscordJSConnector } from "velink/connectors/discordjs";

const connector = new DiscordJSConnector(discordClient, velinkClient);
await connector.init(); // Call after discord client is ready

Handles:

  • VOICE_SERVER_UPDATE events -> Velink
  • VOICE_STATE_UPDATE events -> Velink
  • Voice gateway payload sending
  • Automatic shard calculation

Advanced Usage

Custom Logger

import { setLogger } from "velink";
import pino from "pino";

setLogger(pino({ level: "info" }));

The logger interface:

interface Logger {
  trace(msg: string, ...args: unknown[]): void;
  debug(msg: string, ...args: unknown[]): void;
  info(msg: string, ...args: unknown[]): void;
  warn(msg: string, ...args: unknown[]): void;
  error(msg: string, ...args: unknown[]): void;
  fatal(msg: string, ...args: unknown[]): void;
  child(bindings: Record<string, unknown>): Logger;
}

Direct Voice State Management

If not using DiscordJSConnector, handle events manually:

// Forward Discord voice events to Velink
discord.ws.on("VOICE_SERVER_UPDATE", (data) => {
  velink.voiceServerUpdate(data);
});

discord.ws.on("VOICE_STATE_UPDATE", (data) => {
  velink.voiceStateUpdate(data);
});

// Set payload sender
velink.setSend((guildId, payload) => {
  discord.guilds.cache.get(guildId)?.shard?.send(payload);
});

Queue Management

// Add to end of queue
player.queue.add(track);

// Play next (inserts at front)
player.queue.insertNext(track);

// Shuffle
player.queue.shuffle();

// Get duration
console.log(`Queue duration: ${player.queue.duration}ms`);

// Clear
player.queue.clear();

Custom Load Balancer

velink.on("nodeConnect", () => {
  // Access the internal load balancer
  // (advanced: extend Velink to expose the balancer directly)
});

Migration Guide

From v1.0.x to v1.1.0

  1. Error handling: All errors now extend VelinkError with .code property
  2. Options validation: Constructor now validates options with Zod schemas
  3. Player options: voiceChannelId and textChannelId accept undefined explicitly
  4. Circuit breaker: Metrics available via node.getCircuitMetrics()
  5. Health monitoring: Now built-in with configurable thresholds

Troubleshooting

"No available Lavalink nodes"

  • Check that Lavalink server is running and accessible
  • Verify host, port, and auth credentials
  • Check firewall rules between your bot and Lavalink

"Circuit breaker is OPEN"

  • The node has failed too many times; it will auto-recover
  • Check Lavalink logs for errors
  • Manually reset: node.resetCircuitBreaker()

"Voice connection failed"

  • Ensure the bot has permission to join the voice channel
  • Check that gateway intents include GuildVoiceStates
  • Verify you're forwarding VOICE_SERVER_UPDATE and VOICE_STATE_UPDATE events

High memory usage

  • Reduce playerCleanupThreshold for faster stale player cleanup
  • Limit queue size: new Queue({ maxSize: 1000 })
  • Use unresolved tracks for large playlists: Track.fromUnresolved(title, author, source, logger)

License

MIT (c) Velink Contributors