npm package: @hff-ai/media-processor-client
Description: TypeScript client for communicating with the HFF Media Processor service
Author: HFF Team
Latest version: 1.0.1

# @hff-ai/media-processor-client

A TypeScript client library for communicating with the HFF Media Processor service. Abstracts away AMQP and REST API details, providing both async/await and event-based APIs.

## Features

- **Dual API**: Choose between async/await (`processS3*`) or event-based (`queueS3*`) styles
- **Clean separation**: Async requests don't emit events, preventing double-handling
- **Progress callbacks**: Track processing progress with optional callbacks
- **Auto-reconnect**: Automatic reconnection handling with configurable retry logic
- **Type-safe**: Full TypeScript support with comprehensive type definitions
- **Zero-config MIME detection**: Automatically detects MIME types from file extensions

## Installation

```bash
npm install @hff-ai/media-processor-client
```

Or if using the package locally within the monorepo:

```bash
npm install ./packages/media-processor-client
```

## Quick Start

### Async/Await (Recommended)

```typescript
import { media_processor, ProcessingError } from '@hff-ai/media-processor-client';

const processor = await media_processor.connect({
  processorUrl: 'https://mp.hff.ai',
  processorAmqp: 'amqp://user:pass@mp.hff.ai',
  token: process.env.MEDIA_PROCESSOR_TOKEN!,
  s3Details: {
    provider: 'idrive',
    endpoint: 'https://s3.us-east-1.idrivee2-27.com',
    accessKey: process.env.S3_ACCESS_KEY!,
    secretAccessKey: process.env.S3_SECRET_KEY!,
    region: 'us-east-1',
    bucketName: 'my-media-bucket',
  },
  system: 'my-application',
});

// Process and await result
try {
  const result = await processor.processS3Image('uploads/photo.jpg', {
    timeout: 30000,
    onProgress: (p) => console.log(`${p.progress}%`),
  });
  console.log('Done:', result.result.thumbnailS3Key);
} catch (error) {
  if (error instanceof ProcessingError) {
    console.error(`Failed: ${error.errorCode}`);
  }
}
```

### Event-Based (Fire-and-Forget)

**Important:** Set up handlers BEFORE connecting to capture queued messages.

```typescript
import { media_processor, MediaResultMessage } from '@hff-ai/media-processor-client';

// Create without connecting
const processor = media_processor.create(config);

// Set up handlers FIRST
processor.on('image_completed', (msg: MediaResultMessage) => {
  console.log('Image processed:', msg.result.s3Key);
});

// NOW connect - handlers ready for queued messages
await processor.connect();

// Queue (returns immediately)
const mediaId = processor.queueS3Image('uploads/photo.jpg');
```

## Configuration

### MediaProcessorConfig

| Property        | Type               | Required | Description                              |
| --------------- | ------------------ | -------- | ---------------------------------------- |
| `processorUrl`  | `string`           | Yes      | Base URL of the media processor REST API |
| `processorAmqp` | `string`           | Yes      | AMQP connection URL for RabbitMQ         |
| `token`         | `string`           | Yes      | JWT authentication token                 |
| `s3Details`     | `S3Config`         | Yes      | S3 bucket configuration                  |
| `system`        | `string`           | Yes      | System namespace for queue routing       |
| `options`       | `ProcessorOptions` | No       | Optional processing defaults             |

### S3Config

| Property          | Type     | Required | Description                                             |
| ----------------- | -------- | -------- | ------------------------------------------------------- |
| `provider`        | `string` | Yes      | S3-compatible provider (e.g., 'idrive', 'aws', 'minio') |
| `endpoint`        | `string` | Yes      | S3 endpoint URL                                         |
| `accessKey`       | `string` | Yes      | S3 access key ID                                        |
| `secretAccessKey` | `string` | Yes      | S3 secret access key                                    |
| `region`          | `string` | Yes      | S3 region                                               |
| `bucketName`      | `string` | Yes      | Bucket name                                             |

### ProcessorOptions

| Property               | Type      | Default       | Description                                  |
| ---------------------- | --------- | ------------- | -------------------------------------------- |
| `maxConcurrentJobs`    | `number`  | `5`           | Maximum concurrent processing jobs           |
| `defaultUserId`        | `string`  | `'anonymous'` | Default user ID for requests                 |
| `autoReconnect`        | `boolean` | `true`        | Auto-reconnect on connection loss            |
| `reconnectDelay`       | `number`  | `5000`        | Reconnection delay in milliseconds           |
| `maxReconnectAttempts` | `number`  | `10`          | Maximum reconnection attempts (0 = infinite) |
| `requestTimeout`       | `number`  | `30000`       | Request timeout in milliseconds              |

## API Reference

### Factory Methods

#### `media_processor.connect(config)`

Create and connect to a MediaProcessor instance.

```typescript
const processor = await media_processor.connect(config);
```

#### `media_processor.create(config)`

Create a MediaProcessor instance without connecting. Useful for setting up event handlers before connecting.

```typescript
const processor = media_processor.create(config);
processor.on('connected', () => console.log('Connected!'));
await processor.connect();
```

### Async Processing Methods (Promise-based)

These methods return a Promise that resolves when processing completes.

**Note:** Results from async methods are delivered only via the Promise - no events are emitted. This allows safe mixing of both API styles in the same application.

#### `processor.processS3Image(s3Key, options?): Promise<MediaResultMessage>`

Process an image and await the result.

```typescript
const result = await processor.processS3Image('uploads/photo.jpg', {
  userId: 'user-123',
  timeout: 30000,
  onProgress: (p) => console.log(`${p.progress}%`),
});
console.log('Thumbnail:', result.result.thumbnailS3Key);
```

#### `processor.processS3Video(s3Key, options?): Promise<MediaResultMessage>`

Process a video and await the result.

```typescript
const result = await processor.processS3Video('uploads/video.mp4', {
  userId: 'user-123',
  targetQualities: ['1080p', '720p', '480p'],
  timeout: 300000, // 5 minutes
  onProgress: (p) => updateProgressBar(p.progress),
});
```

#### `processor.processS3Zip(s3Key, options?): Promise<MediaResultMessage>`

Process a zip archive and await the result.

```typescript
const result = await processor.processS3Zip('uploads/photos.zip', {
  userId: 'user-123',
  processImages: true,
  processVideos: true,
  timeout: 120000,
});
```

### Queue Methods (Fire-and-Forget)

These methods return immediately. Results are delivered exclusively via events (`*_completed`, `*_error`, `*_progress`).

#### `processor.queueS3Image(s3Key, options?): string`

Queue an image for processing.

```typescript
const mediaId = processor.queueS3Image('uploads/photo.jpg', {
  userId: 'user-123',
});
// Handle result via processor.on('image_completed', ...)
```

#### `processor.queueS3Video(s3Key, options?): string`

Queue a video for processing.

```typescript
const mediaId = processor.queueS3Video('uploads/video.mp4', {
  userId: 'user-123',
  targetQualities: ['1080p', '720p'],
});
```

#### `processor.queueS3Zip(s3Key, options?): string`

Queue a zip archive for processing.

```typescript
const mediaId = processor.queueS3Zip('uploads/photos.zip', {
  userId: 'user-123',
});
```

### Connection Methods

#### `processor.disconnect()`

Gracefully close all connections.

```typescript
await processor.disconnect();
```

#### `processor.isConnected()`

Check connection status.

```typescript
if (processor.isConnected()) {
  // Safe to submit requests
}
```

#### `processor.reconnect()`

Manually trigger reconnection.

```typescript
await processor.reconnect();
```

#### `processor.updateToken(token)`

Update the authentication token (e.g., after token refresh).

```typescript
processor.updateToken(newToken);
```

#### `processor.getSystemStatus()`

Get current system status and statistics.

```typescript
const status = await processor.getSystemStatus();
console.log(`Pending jobs: ${status.pendingJobs}`);
```

## Events

### Processing Events

| Event              | Payload                | Description                |
| ------------------ | ---------------------- | -------------------------- |
| `image_progress`   | `MediaProgressMessage` | Image processing progress  |
| `image_completed`  | `MediaResultMessage`   | Image processing completed |
| `image_error`      | `MediaErrorMessage`    | Image processing failed    |
| `video_progress`   | `MediaProgressMessage` | Video processing progress  |
| `video_completed`  | `MediaResultMessage`   | Video processing completed |
| `video_error`      | `MediaErrorMessage`    | Video processing failed    |
| `zip_progress`     | `MediaProgressMessage` | Zip extraction progress    |
| `zip_completed`    | `MediaResultMessage`   | Zip processing completed   |
| `zip_error`        | `MediaErrorMessage`    | Zip processing failed      |
| `progress`         | `MediaProgressMessage` | Any media type progress    |
| `completed`        | `MediaResultMessage`   | Any media type completed   |
| `processing_error` | `MediaErrorMessage`    | Any media type failed      |

### Connection Events

| Event          | Payload               | Description             |
| -------------- | --------------------- | ----------------------- |
| `connected`    | `void`                | Successfully connected  |
| `disconnected` | `{ reason: string }`  | Connection lost         |
| `reconnecting` | `{ attempt: number }` | Attempting to reconnect |
| `error`        | `Error`               | Connection-level error  |

### Warning Events

| Event               | Payload                   | Description                                     |
| ------------------- | ------------------------- | ----------------------------------------------- |
| `unhandled_message` | `UnhandledMessagePayload` | Orphaned or unhandled message (will be dropped) |

The `unhandled_message` event fires in two scenarios:

1. **Orphaned async messages**: A `processS3*()` request from a previous process that died. These have a `correlationId` but no matching pending promise. They are NOT sent to regular event handlers.

2. **Queue messages with no listeners**: A `queueS3*()` result where no event handler is registered.

```typescript
processor.on('unhandled_message', (warning) => {
  console.warn(`Dropped: ${warning.message}`);
  // Optionally: save to dead-letter store, alert, etc.
});
```

## Error Handling

The library provides typed error classes for different scenarios:

### Error Classes

```typescript
import {
  MediaProcessorError, // Base class
  ConnectionError, // Connection-related errors
  ConfigurationError, // Configuration errors
  RequestError, // Queue request errors (sync)
  ProcessingError, // Processing failures (async, from process* methods)
} from '@hff-ai/media-processor-client';
```

### Connection Errors

| Code                | Description                    |
| ------------------- | ------------------------------ |
| `CONNECTION_FAILED` | Cannot reach the processor     |
| `CONNECTION_LOST`   | Connection dropped             |
| `AUTH_FAILED`       | Invalid or expired token       |
| `AMQP_ERROR`        | RabbitMQ error                 |
| `REST_API_ERROR`    | REST API error                 |
| `RECONNECT_FAILED`  | Exceeded reconnection attempts |

### Processing Errors

| Code                     | Description                 |
| ------------------------ | --------------------------- |
| `S3_ACCESS_DENIED`       | Insufficient S3 permissions |
| `S3_NOT_FOUND`           | Source file not found       |
| `S3_UPLOAD_FAILED`       | Failed to upload results    |
| `INVALID_FORMAT`         | Invalid file format         |
| `UNSUPPORTED_CODEC`      | Unsupported video codec     |
| `FILE_TOO_LARGE`         | File exceeds size limit     |
| `FILE_CORRUPTED`         | Damaged file                |
| `PROCESSING_TIMEOUT`     | Processing timed out        |
| `TRANSCODING_FAILED`     | Video transcoding failed    |
| `ZIP_EXTRACTION_FAILED`  | Zip extraction failed       |
| `ZIP_PASSWORD_PROTECTED` | Password-protected zip      |

### ProcessingError Codes (async methods)

| Code                 | Description                                              |
| -------------------- | -------------------------------------------------------- |
| `PROCESSING_FAILED`  | Server returned an error (check `errorCode` for details) |
| `PROCESSING_TIMEOUT` | Client-side timeout exceeded                             |
| `DISCONNECTED`       | Connection lost during processing                        |

### Error Handling Example

```typescript
import {
  ConnectionError,
  ProcessingError,
  MediaErrorMessage,
} from '@hff-ai/media-processor-client';

// Async error handling (try/catch)
try {
  const result = await processor.processS3Image('uploads/photo.jpg', {
    timeout: 30000,
  });
} catch (error) {
  if (error instanceof ProcessingError) {
    console.error(`Processing failed: ${error.errorCode} - ${error.message}`);
    if (error.errorCode === 'S3_NOT_FOUND') {
      // Handle missing file
    }
  }
}

// Event-based error handling (for queue methods)
processor.on('image_error', (msg: MediaErrorMessage) => {
  switch (msg.errorCode) {
    case 'S3_NOT_FOUND':
      console.error('File not found:', msg.details);
      break;
    case 'INVALID_FORMAT':
      notifyUser('Invalid image format');
      break;
    default:
      logError(msg);
  }
});

// Connection errors
processor.on('error', (error: ConnectionError) => {
  if (error.code === 'AUTH_FAILED') {
    refreshToken().then((newToken) => {
      processor.updateToken(newToken);
      processor.reconnect();
    });
  }
});
```

## Complete Example

```typescript
import {
  media_processor,
  MediaProcessorConfig,
  MediaResultMessage,
  MediaProgressMessage,
  MediaErrorMessage,
  ConnectionError,
} from '@hff-ai/media-processor-client';

const config: MediaProcessorConfig = {
  processorUrl: process.env.MEDIA_PROCESSOR_URL!,
  processorAmqp: process.env.MEDIA_PROCESSOR_AMQP!,
  token: process.env.MEDIA_PROCESSOR_TOKEN!,
  s3Details: {
    provider: 'idrive',
    endpoint: process.env.S3_ENDPOINT!,
    accessKey: process.env.S3_ACCESS_KEY!,
    secretAccessKey: process.env.S3_SECRET_KEY!,
    region: process.env.S3_REGION || 'us-east-1',
    bucketName: process.env.S3_BUCKET!,
  },
  system: 'my-application',
  options: {
    maxConcurrentJobs: 10,
    autoReconnect: true,
  },
};

async function main() {
  // Create processor with event handlers before connecting
  const processor = media_processor.create(config);

  // Connection events
  processor.on('connected', () => {
    console.log('Connected to media processor');
  });

  processor.on('disconnected', ({ reason }) => {
    console.warn('Disconnected:', reason);
  });

  processor.on('reconnecting', ({ attempt }) => {
    console.log(`Reconnecting (attempt ${attempt})...`);
  });

  processor.on('error', (error: Error) => {
    console.error('Connection error:', error.message);
  });

  // Processing events
  processor.on('image_progress', (msg: MediaProgressMessage) => {
    console.log(`Image ${msg.mediaId}: ${msg.progress}% - ${msg.message}`);
  });

  processor.on('image_completed', (msg: MediaResultMessage) => {
    console.log(`Image completed: ${msg.mediaId}`);
    // Save to database, update UI, etc.
  });

  processor.on('image_error', (msg: MediaErrorMessage) => {
    console.error(`Image failed: ${msg.mediaId} - ${msg.error}`);
  });

  processor.on('video_progress', (msg: MediaProgressMessage) => {
    updateProgressBar(msg.mediaId, msg.progress);
  });

  processor.on('video_completed', (msg: MediaResultMessage) => {
    console.log(`Video completed: ${msg.mediaId}`);
  });

  // Connect
  try {
    await processor.connect();
  } catch (error) {
    if (error instanceof ConnectionError) {
      console.error(`Failed to connect: ${error.code} - ${error.message}`);
    }
    process.exit(1);
  }

  // Process some media
  try {
    const imageId = processor.processS3Image('uploads/photo.jpg', {
      userId: 'user-123',
    });
    console.log(`Processing image: ${imageId}`);

    const videoId = processor.processS3Video('uploads/video.mp4', {
      userId: 'user-123',
      targetQualities: ['1080p', '720p'],
    });
    console.log(`Processing video: ${videoId}`);
  } catch (error) {
    console.error('Failed to submit request:', error);
  }

  // Graceful shutdown
  process.on('SIGINT', async () => {
    console.log('Shutting down...');
    await processor.disconnect();
    process.exit(0);
  });
}

main();
```

## Type Guards

The library provides type guards for narrowing message types:

```typescript
import {
  isProgressMessage,
  isResultMessage,
  isErrorMessage,
  isImageResult,
  isVideoResult,
  isZipResult,
} from '@hff-ai/media-processor-client';

processor.on('completed', (msg) => {
  if (isImageResult(msg.result)) {
    console.log('Image versions:', msg.result.metadata?.imageVersions);
  } else if (isVideoResult(msg.result)) {
    console.log('Duration:', msg.result.duration);
  } else if (isZipResult(msg.result)) {
    console.log('Extracted files:', msg.result.extractedMedia.length);
  }
});
```

## Development

### Building

```bash
cd packages/media-processor-client
npm install
npm run build
```

### Testing

```bash
npm test
```

### Linting

```bash
npm run lint
npm run lint:fix
```

## License

MIT