@mks2508/bun-soulseek
v0.1.0
Published
Soulseek P2P protocol library for Bun
Maintainers
Readme
@bun-soulseek/core
TypeScript/Bun implementation of the Soulseek P2P protocol. Fully typed, modern, and optimized for Bun runtime.
Features
- 🔗 Full Protocol Support: Complete implementation of Soulseek server and peer protocols
- 🎯 Type-Safe: Fully typed with TypeScript strict mode and Zod validation
- 📦 Modern Stack: Built for Bun runtime with ES modules
- 🔄 Dual API: Both event-based and promise-based interfaces
- ⚡ Optimized: Efficient binary message handling with Bun APIs
- 🛡️ Resilient: Built-in retry logic with exponential backoff and circuit breaker pattern
- 📊 Progress Tracking: Real-time download progress with transfer state machine
- 🎨 Excellent Logging: Integrated
@mks2508/better-loggerwith cyberpunk preset
Installation
bun add @bun-soulseek/coreQuick Start
Promise-based API
import { connect } from '@bun-soulseek/core'
const result = await connect('username', 'password')
if (result.ok) {
const client = result.value
// Search for files
const searchResult = await client.search('aphex twin')
if (searchResult.ok) {
console.log(`Found ${searchResult.value.length} results from peers`)
for (const response of searchResult.value) {
for (const file of response.files) {
console.log(` ${file.filename} (${file.size} bytes)`)
// Download a file
const downloadResult = await client.download(file, response.username)
if (downloadResult.ok) {
console.log(`Downloaded: ${file.filename}`)
await Bun.write(`downloads/${file.filename}`, downloadResult.value)
}
}
}
}
client.disconnect()
} else {
console.error(`Connection failed: ${result.error.message}`)
}Event-based API
import { SoulseekClient } from '@bun-soulseek/core'
const client = new SoulseekClient({
searchTimeout: 10000,
})
client.on('ready', () => {
console.log('Connected and authenticated!')
client.search('aphex twin')
})
client.on('search-result', (result) => {
console.log(`Result from ${result.user}: ${result.file}`)
})
client.on('download-progress', (progress) => {
console.log(
`Download: ${progress.percentage.toFixed(1)}% - ${progress.speed / 1024} KB/s`
)
})
client.on('download-complete', (info) => {
console.log(`Download complete: ${info.filename} (${info.size} bytes)`)
})
client.on('error', (error) => {
console.error('Client error:', error)
})
await client.connect('username', 'password')Stream Downloads
For large files, stream directly to disk:
const client = new SoulseekClient()
await client.connect('username', 'password')
const searchResult = await client.search('music file')
if (searchResult.ok) {
const result = searchResult.value[0]
const file = result.files[0]
const stream = client.downloadStream(file, result.username)
const fileWriter = Bun.file(`downloads/${file.filename}`).writer()
const writable = new WritableStream({
write(chunk) {
fileWriter.write(chunk)
},
close() {
fileWriter.end()
},
})
await stream.pipeTo(writable)
}API Reference
SoulseekClient
Main client class for interacting with the Soulseek network.
Constructor
new SoulseekClient(options?: SoulseekClientOptions)Options:
| Property | Type | Default | Description |
| --------------- | -------- | -------------------- | ---------------------------------- |
| host | string | server.slsknet.org | Server hostname |
| port | number | 2242 | Server port |
| listenPort | number | 2234 | Port for incoming peer connections |
| searchTimeout | number | 5000 | Default search timeout in ms |
Methods
connect(username, password)
Connects to Soulseek server and authenticates.
async connect(username: string, password: string): Promise<Result<LoginSuccessData, ResultError>>Returns: Result with login data on success
LoginSuccessData:
{
success: true
greet: string // Server greeting message
ownIp: {
// Your external IP
bytes: [number, number, number, number]
string: string // "x.x.x.x" format
}
passwordHash: string // Password hash for verification
isSupporter: boolean // Whether account has supporter status
}disconnect()
Disconnects from the server and cleans up all resources.
disconnect(): voidsearch(query, options?)
Searches the network for files matching the query.
async search(
query: string,
options?: { timeout?: number }
): Promise<Result<FileSearchResponse[], ResultError>>Parameters:
query- Search query stringoptions.timeout- Override default search timeout (ms)
Returns: Array of search responses from different peers
FileSearchResponse:
{
username: string
token: number
files: SearchResultFile[]
freeSlots: boolean
avgSpeed: number // Average upload speed in bytes/sec
queueLength: number
lockedFiles?: SearchResultFile[]
}SearchResultFile:
{
code: number
filename: string
size: bigint // File size in bytes
extension: string
attributes: {
bitrate?: number // For audio files (kbps)
duration?: number // For audio files (seconds)
vbr?: number
sampleRate?: number
bitDepth?: number
}
}download(file, user, options?)
Downloads a file from a peer.
async download(
file: SearchResultFile,
user: string,
options?: DownloadOptions
): Promise<Result<Uint8Array, ResultError>>Parameters:
file- File metadata from search resultsuser- Username to download fromoptions.path- Optional file path to save directly
Returns: File data as Uint8Array on success
Download Flow:
- Queue transfer in
TransferManager - Request peer address from server
- Connect to peer via
PeerManager - Send
TransferRequestmessage - Wait for
TransferResponsewith allowed status - Create F-type connection for actual file transfer
- Download file bytes and return data
downloadStream(file, user)
Downloads a file as a stream for efficient handling of large files.
downloadStream(file: SearchResultFile, user: string): ReadableStream<Uint8Array>Returns: ReadableStream that emits file chunks
Events:
start- Transfer initiated with expected sizeprogress- Real-time progress updatescomplete- All data receivederror- Transfer failed
Properties
isConnected: boolean
Returns whether the client is connected and authenticated.
Events
| Event | Payload | Description |
| ------------------- | -------------------------- | ---------------------------------- |
| ready | void | Client connected and authenticated |
| disconnected | void | Client disconnected from server |
| error | Error | General error occurred |
| search-result | SearchResult | Individual search result received |
| download-progress | TransferProgress | Download progress update |
| download-complete | { user, filename, size } | Download completed successfully |
SearchResult (event payload):
{
user: string
file: string
size: number
freeSlots: boolean
speed: number
queueLength: number
bitrate?: number
duration?: number
}TransferProgress (event payload):
{
id: string
state: 'queued' | 'transferring' | 'complete' | 'failed'
filename: string
username: string
bytesTransferred: number
totalBytes: number
percentage: number
speed: number // Bytes per second
queuePosition?: number
error?: string
startTime?: number
endTime?: number
}Helper Functions
connect(username, password, options?)
Convenience function to create and connect a client in one call.
async connect(
username: string,
password: string,
options?: SoulseekClientOptions
): Promise<Result<SoulseekClient, ResultError>>Result Pattern
All async methods return Result<T, ResultError> for type-safe error handling
without exceptions.
const result = await client.connect('user', 'pass')
if (result.ok) {
// Success: result.value contains the data
console.log(result.value.greet)
} else {
// Failure: result.error contains structured error info
console.error(`Error: ${result.error.message} (${result.error.code})`)
if (result.error.cause) {
console.error('Caused by:', result.error.cause)
}
}Result Helpers
import { unwrap, unwrapOr, map, flatMap, match } from '@bun-soulseek/core'
// Get value or throw (use sparingly)
const value = unwrap(result)
// Get value or default
const value = unwrapOr(result, defaultValue)
// Transform on success
const mapped = map(result, (value) => value.toUpperCase())
// Chain operations
const chained = flatMap(result, (value) => anotherOperation(value))
// Pattern matching
const output = match(result, {
onOk: (value) => `Success: ${value}`,
onErr: (error) => `Error: ${error.message}`,
})Error Codes
| Category | Code | Description |
| ------------------ | ------------------------ | ------------------------------------ |
| Connection | CONNECTION_FAILED | Could not establish TCP connection |
| | CONNECTION_TIMEOUT | Connection attempt timed out |
| | ALREADY_CONNECTED | Already connected to server |
| | NOT_CONNECTED | Operation requires active connection |
| | INVALID_STATE | Operation invalid in current state |
| Authentication | LOGIN_FAILED | Authentication failed |
| | LOGIN_TIMEOUT | Login took too long |
| | KICKED | Kicked from server |
| Transfer | TRANSFER_FAILED | Transfer operation failed |
| | TRANSFER_DENIED | Peer denied transfer request |
| | TRANSFER_TIMEOUT | Transfer timed out |
| | INCOMPLETE_TRANSFER | Transfer finished with missing bytes |
| Protocol | INVALID_MESSAGE | Malformed protocol message |
| | DECOMPRESSION_FAILED | Could not decompress data |
| | PARSE_ERROR | Failed to parse message |
| Peer | PEER_NOT_FOUND | Peer connection not found |
| | PEER_CONNECTION_FAILED | Could not connect to peer |
| | CIRCUIT_OPEN | Circuit breaker blocking peer |
| General | UNKNOWN | Generic error |
| | CANCELLED | Operation cancelled by user |
Advanced Usage
Direct Server Connection
For advanced control over server communication:
import { ServerConnection } from '@bun-soulseek/core'
const server = new ServerConnection({
host: 'server.slsknet.org',
port: 2242,
listenPort: 2234,
})
const connectResult = await server.connect()
if (connectResult.ok) {
const loginResult = await server.login('username', 'password')
if (loginResult.ok) {
// Listen to server events
server.on('peer-address', (address) => {
console.log(`Peer: ${address.username} @ ${address.ip.string}`)
})
}
}Direct Peer Connection
Connect directly to a peer for P-type communication:
import { PeerManager } from '@bun-soulseek/core'
const peerManager = new PeerManager({
retry: { maxAttempts: 3, baseDelay: 1000 },
circuitBreaker: { failureThreshold: 5, resetTimeout: 60000 },
})
const peerResult = await peerManager.getOrCreatePeer(
'peerUsername',
'192.168.1.100',
'2234',
12345, // token
'myUsername'
)
if (peerResult.ok) {
const peer = peerResult.value
// Request shared file list
peer.requestSharedFileList()
peer.on('shared-files', (data) => {
console.log(`Received file list: ${data.length} bytes`)
})
// Request a file transfer
peer.requestTransfer('path/to/file.mp3')
}Custom Logging
Customize logging configuration:
import { logger } from '@bun-soulseek/core'
// Enable debug mode
process.env.DEBUG = 'slsk:*'
// Change logging preset
logger.preset('minimal')
logger.showTimestamp()
logger.showLocation()
// Or use custom config
logger.updateConfig({
verbosity: 'debug',
colors: false,
timestamp: true,
})Environment Variables
The client will use these environment variables if set:
# Connection settings
SLSK_USER=your_username
SLSK_PASS=your_password
SLSK_HOST=server.slsknet.org
SLSK_PORT=2242
# Debugging
DEBUG=slsk:* # Enable all Soulseek debug logs
DEBUG=slsk:peer # Enable only peer logsArchitecture
Connection Types
The Soulseek protocol uses three connection types:
| Type | Purpose | Usage | | ------------------- | -------------------- | -------------------------------------------- | | Server | Central coordination | Login, search requests, peer discovery | | P (Peer) | P2P negotiation | Transfer requests, search results, user info | | F (File) | Raw file transfer | Actual file byte streaming | | D (Distributed) | Distributed network | Phase 2 - Search network propagation |
Download Flow
Client → Server: GetPeerAddress(username)
Server → Client: PeerAddress { ip, port, token }
Client → Peer (P-type): TransferRequest(token, filename)
Peer → Client (P-type): TransferResponse(allowed, filesize)
Client → Peer (F-type): PeerInit(F-type, token) + [8 zero bytes]
Peer → Client (F-type): [File data bytes...]Resilience Patterns
The library implements several resilience patterns:
Retry Strategy (exponential backoff with jitter):
- Max attempts: 3 (configurable)
- Base delay: 1s
- Max delay: 30s
- Jitter: 10% to avoid thundering herd
Circuit Breaker:
- Failure threshold: 5 failures
- Reset timeout: 60s
- Half-open test requests: 1 success to close
Transfer State Machine:
- Queued → Requesting → Transferring → Complete
- Automatic progress tracking and cleanup
Type System
Key Types
// Client options
interface SoulseekClientOptions {
host?: string
port?: number
listenPort?: number
searchTimeout?: number
}
// Download options
interface DownloadOptions {
path?: string // Save directly to file path
}
// Result type (monad)
type Result<T, E = ResultError> = Ok<T> | Err<E>
interface Ok<T> {
readonly ok: true
readonly value: T
}
interface Err<E> {
readonly ok: false
readonly error: E
}
// File search result
interface FileSearchResponse {
username: string
token: number
files: SearchResultFile[]
freeSlots: boolean
avgSpeed: number
queueLength: number
lockedFiles?: SearchResultFile[]
}
// Search result file
interface SearchResultFile {
code: number
filename: string
size: bigint
extension: string
attributes: Record<string, number>
}
// Download task types (coming soon)
type DownloadStatus =
| 'queued'
| 'connecting'
| 'downloading'
| 'completed'
| 'failed'
| 'cancelled'
| 'paused'
interface TaskProgress {
percent: number // 0-100
speed: number // bytes per second
downloaded: number // bytes downloaded so far
total: number // total file size in bytes
eta?: number // estimated seconds remaining
}
interface DownloadTask {
id: string // UUID
username: string // peer username
filename: string // file path on peer
size: number // file size in bytes
outputPath: string // local save path
priority: number // higher = downloads first
status: DownloadStatus // current state
progress: TaskProgress // download progress
retryCount: number // retry attempts made
maxRetries: number // max retry attempts
createdAt: number // creation timestamp
startedAt?: number // when download started
completedAt?: number // when download ended
error?: string // error message if failed
token?: number // file transfer token
}See types/ directory for complete type definitions.
Development
Build
bun run build:coreType-check
bun run typecheckTest
bun testLint & Format
bun run lint
bun run formatLicense
MIT
Contributing
This is part of the bun-soulseek monorepo. See the main repository for
contribution guidelines.
Links
- Protocol Documentation - Soulseek protocol specification
- Reference Implementations - Node.js and C# implementations
- Main Repository - Monorepo root
