Diagnostics & the glue points
Media emits to nestjs:media:* diagnostics channels with zero coupling; Telescope, Codegen, and React plug in from there.
media is wired into the same four glue points every @dudousxd/nestjs-* library shares. None of them are required to use media — they're how it composes with the rest of the ecosystem when you want it to.
1. Diagnostics channels
Every layer of the library publishes to node:diagnostics_channel with the standard ecosystem envelope. This is the lowest-coupling integration there is: media doesn't import Telescope or know it exists — it just emits, and anything that cares subscribes. When nobody's listening, publishing is a no-op (a cheap hasSubscribers check), so it's always on by default with no measurable cost.
// channel: nestjs:media:<event>
{ ts: number, lib: 'media', event: 'attach', payload: { id, ownerType, ownerId, collection, disk, path, size, mimeType } }Every event
Each event is its own channel (nestjs:media:<event>) carrying a typed payload. The full set (MEDIA_DIAGNOSTIC_EVENTS is exported for convenience):
| Event | Emitted by | Payload |
|---|---|---|
attach | media-library | id, ownerType, ownerId, collection, disk, path, size, mimeType |
delete | media-library | id, ownerType, ownerId |
conversion | media-library | id, conversion, path |
upload.start | resumable uploads | id, disk, key, size?, contentType? |
upload.progress | resumable uploads | id, offset, parts, size? |
upload.complete | resumable uploads | id, disk, key, size |
upload.abort | resumable uploads | id |
attachment.create | attachments | disk, path, size, mimeType, name, variants |
attachment.delete | attachments | disk, path, variants |
This is what makes "do something on upload start / end" a two-line subscription — no hooks API to learn, just the channel:
import { subscribe } from 'node:diagnostics_channel';
import type { MediaDiagnosticEnvelope, UploadCompletePayload } from '@dudousxd/nestjs-media-core';
subscribe('nestjs:media:upload.start', (msg) => {
console.log('upload started', (msg as MediaDiagnosticEnvelope).payload);
});
subscribe('nestjs:media:upload.complete', (msg) => {
const { key, size } = (msg as MediaDiagnosticEnvelope<UploadCompletePayload>).payload;
metrics.observe('media.upload.bytes', size, { key });
});Each emitter takes emitDiagnostics: false to opt out (MediaLibrary, ResumableUploadManager, AttachmentManager). See @dudousxd/nestjs-diagnostics for the cross-ecosystem convention this follows.
2. Telescope
@dudousxd/nestjs-media-telescope ships a MediaWatcher that subscribes to those channels and records a media entry per event — so attaches, deletes, conversions, uploads, and attachments show up in the Telescope console next to your requests, queries, jobs, and mails, correlated under the same batch. It records every milestone but skips per-chunk upload.progress so a large resumable upload doesn't flood the timeline (progress stays on its channel for your own code).
import { MediaWatcher } from '@dudousxd/nestjs-media-telescope';
// add new MediaWatcher() to your telescope watcher listThat's the entire integration: media emits, the watcher records. No changes to your media code, no coupling between the packages beyond the channel name.
Running alongside the generic diagnostics bridge
If you also run @dudousxd/nestjs-diagnostics-telescope's generic bridge, you don't need to hand-maintain an exclude list to avoid double-recording. MediaWatcher.register() claims every event it records via the diagnostics claim registry, so the generic bridge's watcher skips them automatically at record time — you get one Telescope entry per event, not two. The one thing that doesn't change: upload.progress isn't claimed (this watcher doesn't record it either), so if you don't want it flooding the generic bridge too, mute it there yourself with mediaDiagnosticKey('upload.progress') in that bridge's exclude option.
3. Codegen
@dudousxd/nestjs-media-codegen contributes a typed media client to your generated output, so the front-end gets uploadMedia() + mediaUrl() with no hand-written upload protocol. The emitted file is a thin binding over the runtime client (it bakes in your tus base path and re-exports), which is the point of the next section. See Codegen.
4. React
@dudousxd/nestjs-media-react provides useMediaUpload() and <MediaUploader/>, built on the same resumable client — progress, status, and resume handled for you.
One protocol, many surfaces
The tus client lives exactly once, in @dudousxd/nestjs-media-client. The React hook wraps it; the codegen extension re-exports it. There is a single implementation of the wire protocol, so a protocol change is a one-file change — the front-end surfaces follow automatically.
Why this shape
Every glue point here is opt-in and decoupled. media is useful with none of them (it's a storage + media library on its own), better with each of them, and never forced to depend on Telescope, the codegen runtime, or React to function. That's the ecosystem contract: libraries emit on shared channels and ship satellite packages, so you assemble exactly the surface you need.
Attachments (column model)
The adonis-attachment-style persistence model — a file stored as a JSON value object on your own entity's column, instead of in a separate media table. When to reach for it, the Attachment value object, the AttachmentManager API, and how each ORM serializes it.
Overview
The full nestjs-media package set — core, the NestJS module, the browser client, disks, ORM stores, the image engine, and the ecosystem glue points.