npm package: @mks2508/bun-soulseek
Description: Soulseek P2P protocol library for Bun
Author: mks2508
Latest version: 0.1.0

# @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-logger` with cyberpunk
  preset

## Installation

```bash
bun add @bun-soulseek/core
```

## Quick Start

### Promise-based API

```typescript
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

```typescript
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:

```typescript
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

```typescript
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.

```typescript
async connect(username: string, password: string): Promise<Result<LoginSuccessData, ResultError>>
```

**Returns:** Result with login data on success

**LoginSuccessData:**

```typescript
{
  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.

```typescript
disconnect(): void
```

---

##### `search(query, options?)`

Searches the network for files matching the query.

```typescript
async search(
  query: string,
  options?: { timeout?: number }
): Promise<Result<FileSearchResponse[], ResultError>>
```

**Parameters:**

- `query` - Search query string
- `options.timeout` - Override default search timeout (ms)

**Returns:** Array of search responses from different peers

**FileSearchResponse:**

```typescript
{
  username: string
  token: number
  files: SearchResultFile[]
  freeSlots: boolean
  avgSpeed: number          // Average upload speed in bytes/sec
  queueLength: number
  lockedFiles?: SearchResultFile[]
}
```

**SearchResultFile:**

```typescript
{
  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.

```typescript
async download(
  file: SearchResultFile,
  user: string,
  options?: DownloadOptions
): Promise<Result<Uint8Array, ResultError>>
```

**Parameters:**

- `file` - File metadata from search results
- `user` - Username to download from
- `options.path` - Optional file path to save directly

**Returns:** File data as `Uint8Array` on success

**Download Flow:**

1. Queue transfer in `TransferManager`
2. Request peer address from server
3. Connect to peer via `PeerManager`
4. Send `TransferRequest` message
5. Wait for `TransferResponse` with allowed status
6. Create F-type connection for actual file transfer
7. Download file bytes and return data

---

##### `downloadStream(file, user)`

Downloads a file as a stream for efficient handling of large files.

```typescript
downloadStream(file: SearchResultFile, user: string): ReadableStream<Uint8Array>
```

**Returns:** ReadableStream that emits file chunks

**Events:**

- `start` - Transfer initiated with expected size
- `progress` - Real-time progress updates
- `complete` - All data received
- `error` - 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):**

```typescript
{
  user: string
  file: string
  size: number
  freeSlots: boolean
  speed: number
  queueLength: number
  bitrate?: number
  duration?: number
}
```

**TransferProgress (event payload):**

```typescript
{
  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.

```typescript
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.

```typescript
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

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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:

```bash
# 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 logs
```

## Architecture

### 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

```typescript
// 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

```bash
bun run build:core
```

### Type-check

```bash
bun run typecheck
```

### Test

```bash
bun test
```

### Lint & Format

```bash
bun run lint
bun run format
```

## License

MIT

## Contributing

This is part of the `bun-soulseek` monorepo. See the main repository for
contribution guidelines.

## Links

- [Protocol Documentation](../../docs/soulseek-documetation/) - Soulseek
  protocol specification
- [Reference Implementations](../../docs/code-references/) - Node.js and C#
  implementations
- [Main Repository](https://github.com/mks2508/bun-soulseek) - Monorepo root