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

discord-voice-resume

v0.1.0

Published

Survive redeploys: crash-safe snapshot and restore of a discord.js bot's voice presence and playback sessions, with empty-room detection that works around member-cache gaps.

Readme

discord-voice-resume

Survive redeploys. Crash-safe snapshot and restore of a discord.js bot's voice presence and per-guild session state — so a deploy never kicks your bot out of its voice channels or loses what it was playing.

Zero dependencies. discord.js is not even a peer dependency — the client and channel are structurally typed.

The problem

Every discord.js music bot has the same failure mode: you merge to main, CI redeploys, the process restarts, and the bot vanishes from every voice channel it was in. Whatever was playing is gone; whoever parked it in a channel has to re-invite it. Bots either accept this or hand-roll snapshot/restore logic.

This package is that logic, extracted from a production bot (FM-Hachimi) that redeploys on every merge:

  • Crash-safe snapshot file — written synchronously (safe inside a SIGTERM handler), delete-on-read so a crash loop can never replay a poisonous snapshot, TTL so a stale file from last week is discarded, and an optional auto-flush timer so even a hard kill (OOM, docker kill) leaves a fresh snapshot. The auto-flush defers its first write by one interval — state that crashes the process before surviving one interval is never re-persisted.
  • Restore orchestration — fetches and validates each voice channel, detects genuinely empty rooms (see below), isolates per-guild failures, and hands your own opaque payload back to you.
  • Your state stays yours — the package never inspects the payload. Queue, tracks, seek position, loop mode, announcements, and the actual @discordjs/voice join all happen in your onRestore callback.

Usage

import { SessionResume } from 'discord-voice-resume';

interface MyGuildState {
  tracks: SerializedTrack[];
  currentIndex: number;
  loopMode: string;
  positionSeconds: number;
}

const resume = new SessionResume<MyGuildState>({
  enabled: true,
  dataFile: './data/resume.json',   // on a volume that survives the redeploy
  maxAgeMs: 60 * 60 * 1000,
  snapshotIntervalMs: 60_000,       // optional hard-kill safety net
});

On shutdown (inside your SIGTERM handler, before voice connections tear down):

resume.persist(connectedGuildIds, (guildId) => {
  const player = players.get(guildId);
  if (!player?.voiceChannelId) return null;      // not in voice — skip
  return {
    voiceChannelId: player.voiceChannelId,
    textChannelId: player.announceChannelId,
    payload: player.isActive()
      ? { tracks: player.queue.serialize(), currentIndex: player.queue.index, ... }
      : null,                                     // null = presence-only: just rejoin
  };
});

On startup (once the client is ready):

const { restored, skipped } = await resume.restoreAll(client, {
  onRestore: async (voiceChannel, payload, { guildId, textChannelId, roomWasEmpty }) => {
    const player = players.create(guildId);
    if (!(await player.join(voiceChannel))) return false;

    // Presence-only snapshot, or nobody listening: rejoin without playback.
    if (!payload || roomWasEmpty) return true;

    player.queue.load(payload.tracks, payload.currentIndex);
    await player.play({ startAtSeconds: payload.positionSeconds });
    return true;
  },
});

On demandrestoreSession is the core primitive with no file I/O attached, so you can reuse the same rejoin dance live (e.g. an anti-disconnect feature that puts the bot back after someone force-disconnects it):

const state = captureGuildNow(guildId);           // your own fresh capture
await resume.restoreSession(client, state, { onRestore });

restoreSession is deliberately not gated by enabled — that flag only controls the file-based redeploy-resume flow.

Empty-room detection

onRestore receives roomWasEmpty so you can rejoin without blasting audio at nobody. Detecting this correctly is the subtle part, and the reason this isn't just channel.members.size === 0:

Right after startup, channel.members only includes occupants whose GuildMember is cached — and without the GuildMembers intent the member cache holds only the bot itself. Every human gets filtered out and every occupied channel looks empty.

The package counts the guild's voice states instead (populated by GUILD_CREATE even when member objects aren't), excluding the bot's own stale pre-restart state and known bots. An occupant with no cached member counts as human, and unavailable voice states count as "not empty" — a rare resume into a bots-only room beats never resuming at all. The package only reports the fact; whether an empty room downgrades to presence-only is your policy in onRestore.

Events

SessionResume is an EventEmitter — wire these into your own logger/metrics:

| Event | Payload | |---|---| | persist | { guilds, file } | | persist:error | { error } | | restore:success | { guildId, roomWasEmpty, presenceOnly } | | restore:skipped | { guildId, reason } | | restore:error | { guildId?, error } |

Nothing throws past the package boundary: missing/corrupt/expired snapshots, unavailable channels, and throwing callbacks all resolve to skip/false with an event.

Options

| Option | Required | | |---|---|---| | enabled | yes | Gates the file-based flow (persist, consume, restoreAll, auto-snapshot) | | dataFile | yes | Snapshot path — put it on storage that survives the redeploy | | maxAgeMs | yes | Snapshots older than this are discarded on read | | snapshotIntervalMs | no | Auto-flush interval; omit or 0 to disable |

Provenance

Extracted from FM-Hachimi, where this machinery has carried voice sessions across every production redeploy. Sibling packages from the same bot: ytdlp-cookie-keeper, @bryan-kuang/bilibili-audio-extractor. MIT.