@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-emitterRegister 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:
- media.added -- After the file has been saved to storage and the database record has been created, but before conversions start.
- media.conversion-will-start -- Immediately before each conversion is processed.
- media.conversion-completed -- After each conversion is successfully processed and stored.
- media.conversion-failed -- When a conversion fails (the error is caught and logged; subsequent conversions still run).
- media.deleted -- After the database record is removed and files are deleted from storage.
- 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.