NestboltNestbolt

@nestbolt/medialibrary

Events

Listen to media lifecycle events -- uploads, deletions, collection clears, and conversion progress.

The media library emits events for key lifecycle moments. These events let you react to media changes -- for example, to send notifications when media is uploaded, update search indexes when media is deleted, or log conversion failures.

Prerequisites

Events require @nestjs/event-emitter to be installed and registered:

npm install @nestjs/event-emitter

Register the EventEmitterModule in your root module:

import { Module } from "@nestjs/common";
import { EventEmitterModule } from "@nestjs/event-emitter";
import { MediaModule } from "@nestbolt/medialibrary";

@Module({
  imports: [
    EventEmitterModule.forRoot(),
    MediaModule.forRoot({
      // ... your config
    }),
  ],
})
export class AppModule {}

If @nestjs/event-emitter is not installed, the media library works normally but no events are emitted. The event emitter is injected as an optional dependency.

Event Constants

All event names are exported as the MEDIA_EVENTS constant object:

import { MEDIA_EVENTS } from "@nestbolt/medialibrary";

console.log(MEDIA_EVENTS.MEDIA_ADDED);           // "media.added"
console.log(MEDIA_EVENTS.MEDIA_DELETED);          // "media.deleted"
console.log(MEDIA_EVENTS.COLLECTION_CLEARED);     // "media.collection-cleared"
console.log(MEDIA_EVENTS.CONVERSION_WILL_START);  // "media.conversion-will-start"
console.log(MEDIA_EVENTS.CONVERSION_COMPLETED);   // "media.conversion-completed"
console.log(MEDIA_EVENTS.CONVERSION_FAILED);      // "media.conversion-failed"

Use these constants instead of raw strings to avoid typos and enable IDE autocompletion.

Events Reference

media.added

Emitted after a media file has been successfully uploaded, stored, and saved to the database.

Event string: "media.added" Constant: MEDIA_EVENTS.MEDIA_ADDED

Payload: MediaAddedEvent

interface MediaAddedEvent {
  media: MediaEntity;   // The newly created media record
  modelType: string;    // Entity type (e.g., "Post")
  modelId: string;      // Entity ID
}

Example listener:

import { Injectable } from "@nestjs/common";
import { OnEvent } from "@nestjs/event-emitter";
import { MEDIA_EVENTS, MediaAddedEvent } from "@nestbolt/medialibrary";

@Injectable()
export class MediaEventListener {
  @OnEvent(MEDIA_EVENTS.MEDIA_ADDED)
  handleMediaAdded(event: MediaAddedEvent) {
    console.log(
      `New media "${event.media.fileName}" added to ${event.modelType} #${event.modelId}`,
    );
    console.log(`  Collection: ${event.media.collectionName}`);
    console.log(`  Size: ${event.media.humanReadableSize}`);
    console.log(`  Disk: ${event.media.disk}`);
  }
}

media.deleted

Emitted after a single media item has been deleted (record removed and files deleted from storage).

Event string: "media.deleted" Constant: MEDIA_EVENTS.MEDIA_DELETED

Payload: MediaDeletedEvent

interface MediaDeletedEvent {
  media: MediaEntity;   // The deleted media record
  modelType: string;    // Entity type
  modelId: string;      // Entity ID
}

Example listener:

@OnEvent(MEDIA_EVENTS.MEDIA_DELETED)
handleMediaDeleted(event: MediaDeletedEvent) {
  console.log(
    `Media "${event.media.fileName}" deleted from ${event.modelType} #${event.modelId}`,
  );

  // Clean up related resources
  // e.g., remove from search index, invalidate CDN cache
}

media.collection-cleared

Emitted after all media in a collection has been cleared for an entity.

Event string: "media.collection-cleared" Constant: MEDIA_EVENTS.COLLECTION_CLEARED

Payload: CollectionClearedEvent

interface CollectionClearedEvent {
  modelType: string;      // Entity type
  modelId: string;        // Entity ID
  collectionName: string; // The cleared collection name
}

Example listener:

@OnEvent(MEDIA_EVENTS.COLLECTION_CLEARED)
handleCollectionCleared(event: CollectionClearedEvent) {
  console.log(
    `Collection "${event.collectionName}" cleared for ${event.modelType} #${event.modelId}`,
  );
}

media.conversion-will-start

Emitted before an image conversion begins processing.

Event string: "media.conversion-will-start" Constant: MEDIA_EVENTS.CONVERSION_WILL_START

Payload: ConversionWillStartEvent

interface ConversionWillStartEvent {
  media: MediaEntity;      // The media being converted
  conversionName: string;  // Name of the conversion (e.g., "thumbnail")
}

Example listener:

@OnEvent(MEDIA_EVENTS.CONVERSION_WILL_START)
handleConversionStart(event: ConversionWillStartEvent) {
  console.log(
    `Starting conversion "${event.conversionName}" for media ${event.media.id}`,
  );
}

media.conversion-completed

Emitted after an image conversion has been successfully processed and stored.

Event string: "media.conversion-completed" Constant: MEDIA_EVENTS.CONVERSION_COMPLETED

Payload: ConversionCompletedEvent

interface ConversionCompletedEvent {
  media: MediaEntity;      // The media with the completed conversion
  conversionName: string;  // Name of the completed conversion
}

Example listener:

@OnEvent(MEDIA_EVENTS.CONVERSION_COMPLETED)
handleConversionCompleted(event: ConversionCompletedEvent) {
  console.log(
    `Conversion "${event.conversionName}" completed for media ${event.media.id}`,
  );

  // e.g., notify the user that their thumbnail is ready
}

media.conversion-failed

Emitted when an image conversion fails. The original upload is not affected -- the media record is saved and the original file is stored, but the failed conversion is not generated.

Event string: "media.conversion-failed" Constant: MEDIA_EVENTS.CONVERSION_FAILED

Payload: ConversionFailedEvent

interface ConversionFailedEvent {
  media: MediaEntity;      // The media that failed conversion
  conversionName: string;  // Name of the failed conversion
  error: Error;            // The error that occurred
}

Example listener:

@OnEvent(MEDIA_EVENTS.CONVERSION_FAILED)
handleConversionFailed(event: ConversionFailedEvent) {
  console.error(
    `Conversion "${event.conversionName}" failed for media ${event.media.id}: ${event.error.message}`,
  );

  // e.g., send to error tracking service
  // Sentry.captureException(event.error);
}

Complete Listener Example

Here is a complete event listener service that handles all media events:

import { Injectable, Logger } from "@nestjs/common";
import { OnEvent } from "@nestjs/event-emitter";
import {
  MEDIA_EVENTS,
  MediaAddedEvent,
  MediaDeletedEvent,
  CollectionClearedEvent,
  ConversionWillStartEvent,
  ConversionCompletedEvent,
  ConversionFailedEvent,
} from "@nestbolt/medialibrary";

@Injectable()
export class MediaEventListener {
  private readonly logger = new Logger(MediaEventListener.name);

  @OnEvent(MEDIA_EVENTS.MEDIA_ADDED)
  handleMediaAdded(event: MediaAddedEvent): void {
    this.logger.log(
      `Media added: ${event.media.fileName} (${event.media.humanReadableSize}) ` +
      `to ${event.modelType}#${event.modelId} [${event.media.collectionName}]`,
    );
  }

  @OnEvent(MEDIA_EVENTS.MEDIA_DELETED)
  handleMediaDeleted(event: MediaDeletedEvent): void {
    this.logger.log(
      `Media deleted: ${event.media.fileName} ` +
      `from ${event.modelType}#${event.modelId}`,
    );
  }

  @OnEvent(MEDIA_EVENTS.COLLECTION_CLEARED)
  handleCollectionCleared(event: CollectionClearedEvent): void {
    this.logger.log(
      `Collection cleared: "${event.collectionName}" ` +
      `for ${event.modelType}#${event.modelId}`,
    );
  }

  @OnEvent(MEDIA_EVENTS.CONVERSION_WILL_START)
  handleConversionWillStart(event: ConversionWillStartEvent): void {
    this.logger.debug(
      `Conversion starting: "${event.conversionName}" for media ${event.media.id}`,
    );
  }

  @OnEvent(MEDIA_EVENTS.CONVERSION_COMPLETED)
  handleConversionCompleted(event: ConversionCompletedEvent): void {
    this.logger.log(
      `Conversion completed: "${event.conversionName}" for media ${event.media.id}`,
    );
  }

  @OnEvent(MEDIA_EVENTS.CONVERSION_FAILED)
  handleConversionFailed(event: ConversionFailedEvent): void {
    this.logger.error(
      `Conversion failed: "${event.conversionName}" for media ${event.media.id}`,
      event.error.stack,
    );
  }
}

Register the listener in a module:

import { Module } from "@nestjs/common";
import { MediaEventListener } from "./media-event.listener";

@Module({
  providers: [MediaEventListener],
})
export class MediaEventsModule {}

Practical Use Cases

Invalidate CDN Cache on Delete

@OnEvent(MEDIA_EVENTS.MEDIA_DELETED)
async handleMediaDeleted(event: MediaDeletedEvent): Promise<void> {
  const paths = [
    `/${event.media.id}/${event.media.fileName}`,
    `/${event.media.id}/conversions/*`,
  ];
  await this.cdnService.invalidate(paths);
}

Update Search Index on Upload

@OnEvent(MEDIA_EVENTS.MEDIA_ADDED)
async handleMediaAdded(event: MediaAddedEvent): Promise<void> {
  if (event.modelType === "Product") {
    await this.searchService.updateProductImages(event.modelId);
  }
}

Send Notification When Conversion Completes

@OnEvent(MEDIA_EVENTS.CONVERSION_COMPLETED)
async handleConversionCompleted(event: ConversionCompletedEvent): Promise<void> {
  if (event.conversionName === "optimized") {
    await this.notificationService.send(
      event.media.modelId,
      "Your image has been optimized and is ready to view.",
    );
  }
}

Retry Failed Conversions

@OnEvent(MEDIA_EVENTS.CONVERSION_FAILED)
async handleConversionFailed(event: ConversionFailedEvent): Promise<void> {
  this.logger.warn(
    `Scheduling retry for conversion "${event.conversionName}" on media ${event.media.id}`,
  );

  // Queue a retry with a delay
  await this.retryQueue.add("retry-conversion", {
    mediaId: event.media.id,
    conversionName: event.conversionName,
  }, { delay: 30000 }); // retry after 30 seconds
}

Event Timing

Events are emitted at the following points during the media lifecycle:

  1. media.added -- After the file has been saved to storage and the database record has been created, but before conversions start.
  2. media.conversion-will-start -- Immediately before each conversion is processed.
  3. media.conversion-completed -- After each conversion is successfully processed and stored.
  4. media.conversion-failed -- When a conversion fails (the error is caught and logged; subsequent conversions still run).
  5. media.deleted -- After the database record is removed and files are deleted from storage.
  6. media.collection-cleared -- After all items in a collection have been removed.

Note that media.added fires before conversions begin. If you need to react after all conversions are complete, listen for media.conversion-completed and check media.generatedConversions for completeness.