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

raftlink

v1.1.0

Published

A comprehensive, feature-rich, and modern Lavalink v4 client for Node.js

Readme

RaftLink: The Definitive Lavalink V4 Client


RaftLink is engineered to be the most robust and performant Lavalink client available. It provides a seamless, developer-friendly interface for all of Lavalink's capabilities, empowering you to build sophisticated, large-scale music bots with confidence and ease.

Why RaftLink Dominates the Competition

While other clients offer basic functionality, RaftLink is built on a foundation of performance, reliability, and modern features. Here’s how it stands apart:

| Feature | Standard Clients | RaftLink | | ------------------------ | ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | Architecture | Often a simple wrapper around the API. | Intelligent, multi-node architecture with automatic load balancing based on CPU and player count. Ensures optimal performance. | | Reliability | Basic connection handling. | Automatic node reconnection and failover. If a node goes down, RaftLink seamlessly handles it, ensuring uninterrupted service. | | Performance | Standard performance, can struggle under load. | Highly optimized for low memory and CPU usage. Designed for scalability, from small bots to massive, multi-guild services. | | Developer Experience | JavaScript-based, often with basic types. | Natively written in TypeScript with comprehensive, detailed type definitions for a superior, error-free development experience. | | Source Support | Limited to basic sources. | Extensive, out-of-the-box support for YouTube, Spotify, SoundCloud, Apple Music, and more. |

Core Features

  • 👑 Intelligent Node Management: Automatic load balancing and failover across multiple Lavalink nodes.
  • 🚀 High-Performance Playback: Optimized for speed and efficiency, ensuring smooth audio even at scale.
  • 🎵 Extensive Source Support: Natively handles YouTube, Spotify, Apple Music, SoundCloud, and more.
  • 🔧 Robust Event-Driven System: A comprehensive set of events for precise control over your players.
  • 📚 Fully Typed: Written in TypeScript for a superior and safer development experience.
  • ⚙️ Full Lavalink v4 Support: Utilizes all the latest features of the Lavalink API.
  • 📝 Comprehensive Queue System: Advanced queue management with shuffle, remove, and more.

Supported Sources

RaftLink allows you to stream from a vast array of platforms right out of the box:

Installation

Get started with the future of Discord music bots. All you need is Node.js and npm.

npm install raftlink

Getting Started

This example demonstrates how to set up a basic music bot with RaftLink in just a few lines of code.

const { Client, GatewayIntentBits } = require('discord.js');
const { RaftLinkManager, SourceType } = require('raftlink');
const config = require('./config.json');

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

const nodes = [
    {
        host: config.lavalink.host,
        port: config.lavalink.port,
        password: config.lavalink.password,
    },
];

const raftLink = new RaftLinkManager({
    send: (guildId, payload) => {
        const guild = client.guilds.cache.get(guildId);
        if (guild) guild.shard.send(payload);
    },
    nodes: nodes, // Pass nodes in the constructor
});

client.on('ready', () => {
    const botId = client.user.id;
    raftLink.init(botId); // Only pass userId
    console.log(`Bot is ready! Logged in as ${client.user.tag}`);
    console.log('RaftLink initialized with nodes:', nodes);
    console.log('Discord Client Intents:', client.options.intents.toArray()); // Log active intents
});

client.on('raw', (d) => {
    // Only handle VOICE_STATE_UPDATE and VOICE_SERVER_UPDATE events for RaftLink
    if (d.t === 'VOICE_STATE_UPDATE' || d.t === 'VOICE_SERVER_UPDATE') {
        console.log(`[Discord Raw Payload] Handling voice update: Type: ${d.t}, Guild ID: ${d.d ? d.d.guild_id : 'N/A'}, Channel ID: ${d.d ? d.d.channel_id : 'N/A'}`);
        try {
            raftLink.handleVoiceUpdate(d.d);
        } catch (error) {
            console.error(`[Error] Failed to handle voice update for raw payload type ${d.t}:`, error);
        }
    } else {
        // Log other raw Discord events for debugging if needed, but don't pass to RaftLink
        console.log(`[Discord Raw Payload] Ignoring non-voice event: Type: ${d.t}, Guild ID: ${d.d ? d.d.guild_id : 'N/A'}, Channel ID: ${d.d ? d.d.channel_id : 'N/A'}`);
    }
});

// Enhanced handleVoiceUpdate logging within RaftLinkPlayer.ts (already done in RaftLink library)
// Adding a check here for completeness, though the primary issue is Discord not sending the event.
raftLink.on('playerUpdate', (payload) => {
    if (payload.state && payload.state.voice) {
        const { sessionId, token, endpoint } = payload.state.voice;
        if (!sessionId) console.warn(`[RaftLink] [Player] Missing sessionId in playerUpdate voice state for guild ${payload.guildId}`);
        if (!token) console.warn(`[RaftLink] [Player] Missing token in playerUpdate voice state for guild ${payload.guildId}`);
        if (!endpoint) console.warn(`[RaftLink] [Player] Missing endpoint in playerUpdate voice state for guild ${payload.guildId}`);
    }
});

client.on('messageCreate', async (message) => {
    if (message.author.bot) {
        console.log(`[Message] Ignoring bot message from ${message.author.tag}`);
        return;
    }
    if (!message.guild) {
        console.log(`[Message] Ignoring DM from ${message.author.tag}`);
        return;
    }

    console.log(`[Command] Received command "${message.content}" from ${message.author.tag} in guild ${message.guild.name}`);

    if (message.content.startsWith('!play') || message.content.startsWith('!yt') || message.content.startsWith('!sc') || message.content.startsWith('!sp') || message.content.startsWith('!am')) {
        const command = message.content.split(' ')[0];
        const args = message.content.split(' ').slice(1);
        const query = args.join(' ');

        if (!query) {
            console.log(`[Command: ${command}] No query provided by ${message.author.tag}`);
            return message.channel.send('Please provide a song name or URL to play.');
        }

        const voiceChannel = message.member.voice.channel;
        if (!voiceChannel) {
            console.log(`[Command: ${command}] ${message.author.tag} not in a voice channel.`);
            return message.channel.send('You need to be in a voice channel to play music!');
        }

        try {
            let player = raftLink.players.get(message.guild.id);
            console.log(`[Command: ${command}] Current player for guild ${message.guild.id}: ${player ? 'exists' : 'does not exist'}`);

            if (!player) {
                console.log(`[Command: ${command}] Creating new player for guild ${message.guild.id} in channel ${voiceChannel.name}`);
                player = raftLink.createPlayer({
                    guildId: message.guild.id,
                    channelId: voiceChannel.id,
                    textChannelId: message.channel.id,
                });
                console.log(`[Command: ${command}] Attempting to connect to voice channel ${voiceChannel.id}`);
                await player.connect(voiceChannel.id);
                console.log(`[Command: ${command}] Bot connected to voice channel ${voiceChannel.name} in guild ${message.guild.name}.`);

                // Add a small delay to allow Discord to send voice updates
                console.log(`[Command: ${command}] Waiting for 1.5 seconds for Discord voice updates...`);
                await new Promise(resolve => setTimeout(resolve, 1500));
                console.log(`[Command: ${command}] Finished waiting.`);
            } else if (player.channelId !== voiceChannel.id) {
                console.log(`[Command: ${command}] Player already exists in a different channel (${player.channelId}), requested channel ${voiceChannel.id}`);
                return message.channel.send('I am already playing in another voice channel in this guild. Please join my channel or stop the current playback.');
            }

            let source = SourceType.YouTube; // Default to YouTube for !play
            switch (command) {
                case '!yt':
                    source = SourceType.YouTube;
                    break;
                case '!sc':
                    source = SourceType.SoundCloud;
                    break;
                case '!sp':
                    source = SourceType.Spotify;
                    break;
                case '!am':
                    source = SourceType.AppleMusic;
                    break;
            }

            console.log(`[Command: ${command}] Searching for "${query}" on source "${source}" requested by ${message.author.tag}`);
            const res = await raftLink.search(query, message.author, source);
            console.log(`[Command: ${command}] Search result loadType: ${res ? res.loadType : 'N/A'}`);

            if (!res || res.loadType === 'NO_MATCHES' || (res.loadType !== 'PLAYLIST_LOADED' && res.data.length === 0)) {
                console.log(`[Command: ${command}] No results found for query "${query}" on source "${source}"`);
                return message.channel.send('No results found for your query.');
            }

            let tracksToAdd = [];
            if (res.loadType === 'PLAYLIST_LOADED') {
                tracksToAdd = res.data.tracks;
                console.log(`[Command: ${command}] Enqueuing playlist "${res.data.name}" with ${res.data.tracks.length} tracks.`);
                message.channel.send(`Enqueuing playlist **${res.data.name}** with ${res.data.tracks.length} tracks.`);
            } else {
                tracksToAdd = [res.data[0]];
                console.log(`[Command: ${command}] Enqueuing single track "${tracksToAdd[0].info.title}" by "${tracksToAdd[0].info.author}".`);
                message.channel.send(`Enqueuing **${tracksToAdd[0].info.title}** by **${tracksToAdd[0].info.author}**.`);
            }

            const initialQueueSize = player.queue.size;
            player.queue.add(tracksToAdd);
            console.log(`[Command: ${command}] Tracks added to queue. New queue size: ${player.queue.size}`);

            if (!player.playing && !player.paused && initialQueueSize === 0) {
                console.log(`[Command: ${command}] Player not playing, not paused, and queue was empty. Starting playback.`);
                player.play();
            } else {
                console.log(`[Command: ${command}] Player state: playing=${player.playing}, paused=${player.paused}, initialQueueSize=${initialQueueSize}. Not starting playback.`);
            }
        } catch (error) {
            console.error(`[Error] Command ${command} failed for guild ${message.guild.id}:`, error);
            if (error.message.includes('Missing access') || error.message.includes('Not permitted')) {
                message.channel.send('I do not have permission to connect to your voice channel. Please check my permissions.');
            } else if (error.message.includes('Connection timed out')) {
                message.channel.send('Failed to connect to the voice channel. The connection timed out.');
            } else {
                message.channel.send(`An error occurred while trying to play the song: ${error.message}`);
            }
        }
    } else if (message.content === '!stop') {
        const player = raftLink.players.get(message.guild.id);
        if (player) {
            console.log(`[Command: !stop] Stopping player for guild ${message.guild.id}`);
            player.destroy();
            raftLink.players.delete(message.guild.id);
            message.channel.send('Stopped playback and cleared the queue.');
        } else {
            console.log(`[Command: !stop] No player found for guild ${message.guild.id}`);
            message.channel.send('No music is currently playing.');
        }
    } else if (message.content === '!skip') {
        const player = raftLink.players.get(message.guild.id);
        if (player && player.queue.size > 0) {
            console.log(`[Command: !skip] Skipping track for guild ${message.guild.id}`);
            player.stop();
            message.channel.send('Skipped the current track.');
        } else {
            console.log(`[Command: !skip] No track to skip or queue is empty for guild ${message.guild.id}`);
            message.channel.send('No track to skip or queue is empty.');
        }
    } else if (message.content === '!queue') {
        const player = raftLink.players.get(message.guild.id);
        if (player && player.queue.size > 0) {
            const queueList = player.queue.map((track, index) => `${index + 1}. ${track.info.title}`).join('\n');
            console.log(`[Command: !queue] Displaying queue for guild ${message.guild.id}. Size: ${player.queue.size}`);
            message.channel.send(`**Current Queue:**\n${queueList}`);
        } else {
            console.log(`[Command: !queue] Queue is empty for guild ${message.guild.id}`);
            message.channel.send('The queue is empty.');
        }
    }
});

// RaftLink Player Events for detailed error logging
raftLink.on('nodeConnect', node => {
    console.log(`[RaftLink Event] Node "${node.options.host}" connected.`);
});

raftLink.on('nodeDisconnect', (node, code, reason) => { // Added 'code' parameter
    console.error(`[RaftLink Event] Node "${node.options.host}" disconnected. Code: ${code}, Reason: ${reason || 'Unknown'}`);
    for (const player of raftLink.players.values()) {
        if (player.node === node) {
            console.log(`[RaftLink Event] Informing guild ${player.guildId} about node disconnect.`);
            client.channels.cache.get(player.textChannelId).send(`The music service (Lavalink) node **${node.options.host}** disconnected. Playback might be interrupted. Reason: ${reason || 'Unknown'}`);
            player.destroy();
            raftLink.players.delete(player.guildId);
        }
    }
});

raftLink.on('nodeError', (node, error) => {
    console.error(`[RaftLink Event] Node "${node.options.host}" encountered an error:`, error);
    for (const player of raftLink.players.values()) {
        if (player.node === node) {
            console.log(`[RaftLink Event] Informing guild ${player.guildId} about node error.`);
            client.channels.cache.get(player.textChannelId).send(`The music service (Lavalink) node **${node.options.host}** encountered an error: ${error.message}. Playback might be affected.`);
        }
    }
});

raftLink.on('trackStart', (player, track) => {
    console.log(`[RaftLink Event] Now playing: "${track.info.title}" in guild ${player.guildId}.`);
    client.channels.cache.get(player.textChannelId).send(`Now playing: **${track.info.title}**`);
});

raftLink.on('trackEnd', (player, track, reason) => {
    console.log(`[RaftLink Event] Track ended: "${track ? track.info.title : 'Unknown'}" in guild ${player.guildId}. Reason: ${reason}.`);
    if (reason === 'REPLACED') {
        console.log('[RaftLink Event] Track was replaced, not playing next.');
        return;
    }
    if (player.queue.size > 0) {
        console.log('[RaftLink Event] Queue not empty, playing next track.');
        player.play();
    } else {
        console.log('[RaftLink Event] Queue finished. Destroying player.');
        client.channels.cache.get(player.textChannelId).send('Queue finished.');
        player.destroy();
        raftLink.players.delete(player.guildId);
    }
});

raftLink.on('trackStuck', (player, track, threshold) => {
    console.error(`[RaftLink Event] Track stuck: "${track.info.title}" in guild ${player.guildId}. Threshold: ${threshold}.`);
    client.channels.cache.get(player.textChannelId).send(`Error: Track **${track.info.title}** got stuck after ${threshold / 1000} seconds. Skipping to the next track.`);
    player.stop();
});

raftLink.on('trackError', (player, track, error) => {
    console.error(`[RaftLink Event] Track error: "${track ? track.info.title : 'Unknown'}" in guild ${player.guildId}. Error:`, error);
    client.channels.cache.get(player.textChannelId).send(`Error playing **${track ? track.info.title : 'Unknown'}**: ${error.message}. Skipping to the next track.`);
    player.stop();
});

raftLink.on('socketClosed', async (player, payload) => {
    console.error(`[RaftLink Event] Socket closed for guild ${player.guildId}. Code: ${payload.code}, Reason: ${payload.reason}, By Remote: ${payload.byRemote}.`);
    if (payload.byRemote) {
        console.log(`[RaftLink Event] Attempting to reconnect for guild ${player.guildId}...`);
        client.channels.cache.get(player.textChannelId).send(`Voice connection closed unexpectedly. Code: ${payload.code}, Reason: ${payload.reason}. Attempting to reconnect...`);
        try {
            await player.connect(player.channelId);
            console.log(`[RaftLink Event] Successfully reconnected for guild ${player.guildId}.`);
            client.channels.cache.get(player.textChannelId).send(`Successfully reconnected to voice channel.`);
        } catch (error) {
            console.error(`[RaftLink Event] Failed to reconnect for guild ${player.guildId}:`, error);
            client.channels.cache.get(player.textChannelId).send(`Failed to reconnect to voice channel. Please try playing a song again.`);
            player.destroy();
            raftLink.players.delete(player.guildId);
        }
    } else {
        console.log(`[RaftLink Event] Socket closed by bot for guild ${player.guildId}. Destroying player.`);
        player.destroy();
        raftLink.players.delete(player.guildId);
    }
});

client.login(config.discord_bot_token).catch(err => {
    console.error('[Error] Failed to login to Discord:', err);
});

config.json

  "discord_bot_token": "bot_token",
  "lavalink": {
    "host": "host",
    "port": port,
    "password": "password"
  }
}

API Reference

RaftLinkManager

The central hub for managing nodes and players.

new RaftLinkManager(options: ManagerOptions)

Creates a new manager instance.

  • options.nodes: NodeOptions[] - An array of Lavalink node configurations.
  • options.send: (guildId: string, payload: any) => void - The function to send voice payloads to Discord.
  • options.userId: string (optional) - The bot's user ID. Can be set later with init().

raftLink.init(userId: string)

Initializes the manager and connects to the provided nodes. Must be called after your bot is ready.

raftLink.createPlayer(options: PlayerOptions): RaftLinkPlayer

Creates a new player for a guild or returns an existing one.

  • options.guildId: string
  • options.channelId: string (Voice Channel ID)
  • options.textChannelId: string (optional, for bot messages)

raftLink.search(query: string, requester: any, source?: SourceType): Promise<LoadTracksResult>

Searches for tracks.

  • source: Use the SourceType enum (e.g., SourceType.YouTube, SourceType.Spotify) for specific platform searches.

RaftLinkPlayer

Represents a single guild's music player.

Properties

  • queue: Queue - The guild's song queue.
  • playing: boolean - Whether the player is currently playing.
  • paused: boolean - Whether the player is paused.
  • currentTrack: Track | null - The currently playing track.

Methods

  • connect(channelId: string, options?: { selfDeaf?: boolean; selfMute?: boolean }): Connects to a voice channel.
  • disconnect(): Disconnects from the voice channel.
  • destroy(): Disconnects, clears the queue, and removes the player instance.
  • play(track?: Track): Starts playing the next track in the queue.
  • pause(state: boolean = true): Pauses or resumes the player.
  • stop(): Stops playback and clears the player.
  • seek(position: number): Seeks to a position in the current track (in milliseconds).
  • setVolume(volume: number): Sets the volume (0-1000).

Events

Harness RaftLink's event-driven architecture for full control.

Manager Events

  • nodeConnect(node: RaftLinkNode)
  • nodeDisconnect(node: RaftLinkNode, code: number, reason: string)
  • nodeError(node: RaftLinkNode, error: Error)
  • nodeReady(node: RaftLinkNode, payload: any)

Player Events

  • trackStart(player: RaftLinkPlayer, track: Track)
  • trackEnd(player: RaftLinkPlayer, track: Track, payload: any)
  • trackException(player: RaftLinkPlayer, track: Track, payload: any)
  • trackStuck(player: RaftLinkPlayer, track: Track, payload: any)
  • queueEnd(player: RaftLinkPlayer)

Contributing

Contributions are what make the open-source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.

  1. Fork the Project
  2. Create your Feature Branch (git checkout -b feature/AmazingFeature)
  3. Commit your Changes (git commit -m 'Add some AmazingFeature')
  4. Push to the Branch (git push origin feature/AmazingFeature)
  5. Open a Pull Request

License

Distributed under the MIT License. See LICENSE for more information.