Files
dc-recorder/docs/superpowers/plans/2026-05-15-selfbot-performance-feature-optimization.md

18 KiB

Selfbot Performance Feature Optimization Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Optimize the app's selfbot client runtime and vendor internals for lower memory pressure, safer REST retries, reduced voice cleanup leaks, faster gateway queue processing, and lightweight observability.

Architecture: Start with app-level client options because they are low-risk and immediately reduce cache pressure. Then patch vendor internals in isolated areas: REST manager/request handling, voice packet cleanup, and WebSocket shard queueing. Keep public imports and runtime APIs compatible with discord.js-selfbot-v13 consumers.

Tech Stack: Node.js, TypeScript, CommonJS vendor package, discord.js-selfbot-v13 workspace dependency, Undici, Vitest, Biome, TypeScript.


File Structure

  • Modify src/index.ts: instantiate Client with low-memory cache/sweeper/REST options.
  • Modify vendor/discord.js-selfbot-v13/src/rest/RESTManager.js: own per-client dispatcher state and super-properties cache helpers.
  • Modify vendor/discord.js-selfbot-v13/src/rest/APIRequest.js: use per-client dispatcher and cached x-super-properties header.
  • Modify vendor/discord.js-selfbot-v13/src/rest/RequestHandler.js: add backoff/jitter helper and debug telemetry for retry attempts.
  • Modify vendor/discord.js-selfbot-v13/src/client/voice/receiver/PacketHandler.js: clear speaking timers and reduce RTP parse allocations.
  • Modify vendor/discord.js-selfbot-v13/src/client/websocket/WebSocketShard.js: use cursor-backed gateway queue with compatible priority insertion and destroy cleanup.
  • Create tests/vendor/selfbotClientOptions.test.ts: verify app client options factory if extracted.
  • Create tests/vendor/requestHandlerBackoff.test.ts: verify retry delay calculation is bounded and grows.
  • Create tests/vendor/websocketQueue.test.ts: verify FIFO, priority, and destroy queue reset semantics for the new queue helpers if exported/testable.

Task 1: Extract and Test Low-Memory Client Options

Files:

  • Create: src/discordClientOptions.ts

  • Modify: src/index.ts:4-25

  • Test: tests/vendor/selfbotClientOptions.test.ts

  • Step 1: Write the failing test

Create tests/vendor/selfbotClientOptions.test.ts:

import { describe, expect, it } from "vitest";
import { createDiscordClientOptions } from "../../src/discordClientOptions";

describe("createDiscordClientOptions", () => {
  it("uses low-memory message cache and active sweepers", () => {
    const options = createDiscordClientOptions();

    expect(options.restRequestTimeout).toBe(15_000);
    expect(options.retryLimit).toBe(2);
    expect(options.restGlobalRateLimit).toBe(45);
    expect(options.sweepers).toEqual({
      messages: { interval: 300, lifetime: 600 },
      threads: { interval: 3600, lifetime: 14400 },
    });

    expect(options.partials).toEqual(["USER", "CHANNEL", "GUILD_MEMBER", "MESSAGE"]);
  });
});
  • Step 2: Run the test to verify it fails

Run: pnpm exec vitest run tests/vendor/selfbotClientOptions.test.ts

Expected: FAIL with module not found for src/discordClientOptions.

  • Step 3: Add the client options factory

Create src/discordClientOptions.ts:

import { Options } from "discord.js-selfbot-v13";

export function createDiscordClientOptions() {
  return {
    makeCache: Options.cacheWithLimits({
      ...Options.defaultMakeCacheSettings,
      MessageManager: 25,
      ReactionManager: 0,
      ReactionUserManager: 0,
      PresenceManager: 0,
    }),
    partials: ["USER", "CHANNEL", "GUILD_MEMBER", "MESSAGE"],
    sweepers: {
      messages: { interval: 300, lifetime: 600 },
      threads: { interval: 3600, lifetime: 14400 },
    },
    restRequestTimeout: 15_000,
    retryLimit: 2,
    restGlobalRateLimit: 45,
  };
}
  • Step 4: Use the factory in the app entry point

Modify src/index.ts:

import { Client } from "discord.js-selfbot-v13";
import { config } from "./config";
import { closeDatabase, initializeDatabase } from "./database/drizzle";
import { createDiscordClientOptions } from "./discordClientOptions";

Replace:

const client = new Client();

with:

const client = new Client(createDiscordClientOptions());
  • Step 5: Run the focused test

Run: pnpm exec vitest run tests/vendor/selfbotClientOptions.test.ts

Expected: PASS.

  • Step 6: Run typecheck

Run: pnpm run typecheck

Expected: PASS. If TypeScript cannot type Options from the vendor package, add a local return type only if necessary; do not weaken the factory to any.

  • Step 7: Commit

Run:

git add src/discordClientOptions.ts src/index.ts tests/vendor/selfbotClientOptions.test.ts
git commit -m "perf: tune selfbot client runtime options"

Task 2: Add Per-Client REST Dispatcher and Cached Super Properties

Files:

  • Modify: vendor/discord.js-selfbot-v13/src/rest/RESTManager.js:1-69

  • Modify: vendor/discord.js-selfbot-v13/src/rest/APIRequest.js:1-166

  • Step 1: Add REST manager state

Modify vendor/discord.js-selfbot-v13/src/rest/RESTManager.js imports:

const { Collection } = require('@discordjs/collection');
const makeFetchCookie = require('fetch-cookie');
const { CookieJar } = require('tough-cookie');
const { buildConnector, Client: UndiciClient, ProxyAgent, fetch: fetchOriginal } = require('undici');
const APIRequest = require('./APIRequest');
const routeBuilder = require('./APIRouter');
const RequestHandler = require('./RequestHandler');
const { Error } = require('../errors');
const { ciphers } = require('../util/Constants');
const { Endpoints } = require('../util/Constants');
const Util = require('../util/Util');
  • Step 2: Add per-client dispatcher fields and helper methods

Inside RESTManager constructor after this.fetch = ..., add:

this.dispatcher = null;
this.superPropertiesSource = null;
this.superPropertiesHeader = null;

Add methods before request(method, url, options = {}):

getDispatcher() {
  if (this.dispatcher) return this.dispatcher;

  const proxy = Util.checkUndiciProxyAgent(this.client.options.http.agent);
  if (proxy) {
    this.dispatcher = new ProxyAgent({
      ...proxy,
      ciphers: ciphers.join(':'),
    });
  } else {
    this.dispatcher = new UndiciClient('https://discord.com', {
      connect: buildConnector({ ciphers: ciphers.join(':') }),
    });
  }

  return this.dispatcher;
}

getSuperPropertiesHeader() {
  const source = JSON.stringify(this.client.options.ws.properties);
  if (source !== this.superPropertiesSource) {
    this.superPropertiesSource = source;
    this.superPropertiesHeader = Buffer.from(source, 'ascii').toString('base64');
  }
  return this.superPropertiesHeader;
}
  • Step 3: Remove module-global dispatcher from APIRequest

Modify vendor/discord.js-selfbot-v13/src/rest/APIRequest.js imports to:

const Buffer = require('node:buffer').Buffer;
const { setTimeout } = require('node:timers');
const { FormData } = require('undici');

Remove:

const { FormData, buildConnector, Client, ProxyAgent } = require('undici');
const { ciphers } = require('../util/Constants');
const Util = require('../util/Util');

let agent = null;
  • Step 4: Use REST manager dispatcher and cached header

In APIRequest.make, delete the if (!agent) { ... } block.

Replace the x-super-properties header construction with:

'x-super-properties': this.rest.getSuperPropertiesHeader(),

Replace fetch dispatcher:

dispatcher: agent,

with:

dispatcher: this.rest.getDispatcher(),
  • Step 5: Run vendor lint through root lint

Run: pnpm run lint

Expected: PASS or existing unrelated lint failures. If failures are in edited vendor files, fix them.

  • Step 6: Commit

Run:

git add vendor/discord.js-selfbot-v13/src/rest/RESTManager.js vendor/discord.js-selfbot-v13/src/rest/APIRequest.js
git commit -m "perf: cache selfbot rest dispatcher metadata"

Task 3: Add REST Retry Backoff With Jitter

Files:

  • Modify: vendor/discord.js-selfbot-v13/src/rest/RequestHandler.js:1-505

  • Test: tests/vendor/requestHandlerBackoff.test.ts

  • Step 1: Export a pure backoff helper for tests

Add near the top of vendor/discord.js-selfbot-v13/src/rest/RequestHandler.js after calculateReset:

function calculateRetryDelay(retryCount, random = Math.random) {
  const base = 250;
  const max = 5_000;
  const exponential = Math.min(max, base * 2 ** Math.max(0, retryCount - 1));
  return exponential + Math.floor(random() * base);
}

At the bottom, replace:

module.exports = RequestHandler;

with:

module.exports = RequestHandler;
module.exports.calculateRetryDelay = calculateRetryDelay;
  • Step 2: Write the focused helper test

Create tests/vendor/requestHandlerBackoff.test.ts:

import { describe, expect, it } from "vitest";

const { calculateRetryDelay } = await import(
  "../../vendor/discord.js-selfbot-v13/src/rest/RequestHandler.js"
);

describe("calculateRetryDelay", () => {
  it("increases exponentially and applies bounded jitter", () => {
    expect(calculateRetryDelay(1, () => 0)).toBe(250);
    expect(calculateRetryDelay(2, () => 0)).toBe(500);
    expect(calculateRetryDelay(3, () => 0)).toBe(1000);
    expect(calculateRetryDelay(10, () => 0)).toBe(5000);
    expect(calculateRetryDelay(1, () => 0.999)).toBe(499);
  });
});
  • Step 3: Run the helper test

Run: pnpm exec vitest run tests/vendor/requestHandlerBackoff.test.ts

Expected: PASS.

  • Step 4: Apply backoff to network errors

In RequestHandler.execute, replace the catch block after request.make(...) with:

    } catch (error) {
      if (request.retries === this.manager.client.options.retryLimit) {
        throw new HTTPError(
          error.message,
          error.constructor.name,
          error.status,
          request,
        );
      }

      request.retries++;
      const delay = calculateRetryDelay(request.retries);
      this.manager.client.emit(
        DEBUG,
        `[Request Handler] Retrying failed request after ${delay}ms.\n  Method : ${request.method}\n  Path   : ${request.path}\n  Route  : ${request.route}\n  Retry  : ${request.retries}`,
      );
      await sleep(delay);
      return this.execute(request);
    }
  • Step 5: Apply backoff to 5xx responses

In the 5xx block, replace:

      request.retries++;
      return this.execute(request);

with:

      request.retries++;
      const delay = calculateRetryDelay(request.retries);
      this.manager.client.emit(
        DEBUG,
        `[Request Handler] Retrying server error after ${delay}ms.\n  Method : ${request.method}\n  Path   : ${request.path}\n  Route  : ${request.route}\n  Status : ${res.status}\n  Retry  : ${request.retries}`,
      );
      await sleep(delay);
      return this.execute(request);
  • Step 6: Run focused and full tests

Run: pnpm exec vitest run tests/vendor/requestHandlerBackoff.test.ts

Expected: PASS.

Run: pnpm run test

Expected: PASS.

  • Step 7: Commit

Run:

git add vendor/discord.js-selfbot-v13/src/rest/RequestHandler.js tests/vendor/requestHandlerBackoff.test.ts
git commit -m "perf: back off selfbot rest retries"

Task 4: Clean Voice Receiver Timers and Reduce RTP Buffer Work

Files:

  • Modify: vendor/discord.js-selfbot-v13/src/client/voice/receiver/PacketHandler.js:1-280

  • Step 1: Patch AES decrypt concat allocation

In parseBuffer, replace:

        packet = Buffer.concat([
          decipheriv.update(encrypted),
          decipheriv.final(),
        ]);

with:

        const updated = decipheriv.update(encrypted);
        const final = decipheriv.final();
        packet = final.length === 0 ? updated : Buffer.concat([updated, final]);
  • Step 2: Patch XChaCha auth tag concat allocation

Replace:

          Buffer.concat([encrypted, authTag]),

with:

          buffer.subarray(headerSize, buffer.length - UNPADDED_NONCE_LENGTH),
  • Step 3: Add speaking timeout cleanup

In destroyAllStream(), after clearing video streams, add:

    for (const timeout of this.speakingTimeouts.values()) {
      clearTimeout(timeout);
    }
    const clearedSpeakingTimeouts = this.speakingTimeouts.size;
    this.speakingTimeouts.clear();
    this.emit('debug', {
      message: 'Destroyed voice receiver streams',
      audioStreams: this.streams.size,
      videoStreams: this.videoStreams.size,
      speakingTimeouts: clearedSpeakingTimeouts,
    });

Then adjust ordering so the counts are captured before streams.clear() and videoStreams.clear():

  destroyAllStream() {
    const audioStreams = this.streams.size;
    const videoStreams = this.videoStreams.size;
    for (const stream of this.streams.values()) {
      stream.stream.destroy();
    }
    this.streams.clear();
    for (const stream of this.videoStreams.values()) {
      stream.destroy();
    }
    this.videoStreams.clear();
    for (const timeout of this.speakingTimeouts.values()) {
      clearTimeout(timeout);
    }
    const speakingTimeouts = this.speakingTimeouts.size;
    this.speakingTimeouts.clear();
    this.emit('debug', {
      message: 'Destroyed voice receiver streams',
      audioStreams,
      videoStreams,
      speakingTimeouts,
    });
  }
  • Step 4: Run lint

Run: pnpm run lint

Expected: PASS or only unrelated existing failures. Fix edited-file failures.

  • Step 5: Run tests

Run: pnpm run test

Expected: PASS.

  • Step 6: Commit

Run:

git add vendor/discord.js-selfbot-v13/src/client/voice/receiver/PacketHandler.js
git commit -m "perf: clean up selfbot voice receiver state"

Task 5: Replace Gateway Queue Shift With Cursor Queue

Files:

  • Modify: vendor/discord.js-selfbot-v13/src/client/websocket/WebSocketShard.js:108-120,818-954

  • Step 1: Add queue cursor metadata

In the ratelimit object, change:

queue: [],

To:

queue: [],
queueOffset: 0,
  • Step 2: Update priority insertion

Replace send(data, important = false) with:

  send(data, important = false) {
    if (important) {
      if (this.ratelimit.queueOffset === 0) {
        this.ratelimit.queue.unshift(data);
      } else {
        this.ratelimit.queue[--this.ratelimit.queueOffset] = data;
      }
    } else {
      this.ratelimit.queue.push(data);
    }
    this.processQueue();
  }
  • Step 3: Update queue processing

Replace processQueue() with:

  processQueue() {
    if (this.ratelimit.remaining === 0) return;
    if (this.ratelimit.queueOffset >= this.ratelimit.queue.length) return;
    if (this.ratelimit.remaining === this.ratelimit.total) {
      this.ratelimit.timer = setTimeout(() => {
        this.ratelimit.remaining = this.ratelimit.total;
        this.processQueue();
      }, this.ratelimit.time).unref();
    }
    while (this.ratelimit.remaining > 0) {
      const item = this.ratelimit.queue[this.ratelimit.queueOffset++];
      if (!item) {
        this._compactQueue();
        return;
      }
      this._send(item);
      this.ratelimit.remaining--;
    }
    this._compactQueue();
  }
  • Step 4: Add queue compaction helper

Add before destroy(...):

  _compactQueue() {
    if (this.ratelimit.queueOffset === 0) return;
    if (this.ratelimit.queueOffset >= this.ratelimit.queue.length) {
      this.ratelimit.queue.length = 0;
      this.ratelimit.queueOffset = 0;
      return;
    }
    if (this.ratelimit.queueOffset > 512) {
      this.ratelimit.queue = this.ratelimit.queue.slice(this.ratelimit.queueOffset);
      this.ratelimit.queueOffset = 0;
    }
  }
  • Step 5: Reset cursor on destroy

In destroy, after:

this.ratelimit.queue.length = 0;

Add:

this.ratelimit.queueOffset = 0;
  • Step 6: Run lint and tests

Run: pnpm run lint

Expected: PASS or only unrelated existing failures. Fix edited-file failures.

Run: pnpm run test

Expected: PASS.

  • Step 7: Commit

Run:

git add vendor/discord.js-selfbot-v13/src/client/websocket/WebSocketShard.js
git commit -m "perf: optimize selfbot gateway send queue"

Task 6: Final Verification and Manual Runtime Notes

Files:

  • Modify only if verification exposes issues.

  • Step 1: Run full lint

Run: pnpm run lint

Expected: PASS.

  • Step 2: Run full typecheck

Run: pnpm run typecheck

Expected: PASS.

  • Step 3: Run full tests

Run: pnpm run test

Expected: PASS.

  • Step 4: Run build

Run: pnpm run build

Expected: PASS.

  • Step 5: Inspect git diff

Run: git diff --stat HEAD~5..HEAD if each task was committed, or git diff --stat if not.

Expected: changes limited to app client options, vendor REST, vendor voice, vendor WebSocket, tests, and this plan/spec.

  • Step 6: Record manual Discord runtime limitation

If no Discord token/runtime environment is available, final response must state:

Automated verification passed. I could not perform live Discord runtime verification in this environment. Manual checks still needed: login, message capture, backlog sync, voice connect, voice record, disconnect, reconnect.
  • Step 7: Commit verification fixes only if needed

If Step 1-4 required fixes, commit only those fixes:

git add <fixed-files>
git commit -m "fix: stabilize selfbot optimization verification"

Self-Review

  • Spec coverage: app runtime config is Task 1; REST dispatcher/header/backoff is Tasks 2-3; voice cleanup/allocation is Task 4; gateway queue is Task 5; verification/manual runtime note is Task 6.
  • Placeholder scan: no TBD/TODO/fill-in steps remain; each code step includes concrete snippets and paths.
  • Type consistency: createDiscordClientOptions, calculateRetryDelay, getDispatcher, getSuperPropertiesHeader, _compactQueue, and queueOffset are introduced before use and named consistently.