# `metadata/metadata` _(no submodules)_ ## Purpose `MetadataTransform` is the high-level composing wrapper around `MimeTransform` and `ExifTransform`. A single instance runs MIME detection followed by EXIF extraction in sequence; the result is a fully enriched payload carrying both `mime` and `exif`. Child events are forwarded through the wrapper so a single `on('mime:detected', fn)` subscription on the wrapper observes the child emission — no need to wire per-transform listeners. If either child throws, the wrapper catches the exception, emits `metadata:error` with the thrown value, and resolves with the original input reference so the pipeline continues. This is the entry point most callers should reach for; the individual transforms are exposed for manual composition or when only one stage is desired. ## Features - Composes `MimeTransform` then `ExifTransform` with shared mode flag. - Forwards `mime:detected` and `exif:detected` events to the wrapper's listeners (no child subscriptions required). - Emits `metadata:complete` on success with the final enriched payload. - Emits `metadata:error` with the thrown value on child failure and returns the original input reference so a downstream pipeline stage can continue. - Overrides `dispose()` to release child transforms in addition to the wrapper's own listener / hook tables. ## Usage: Basic Example ```ts import { MetadataTransform } from "@suaveplan/streams-metadata"; const transform = new MetadataTransform(); const result = await transform.process({ bytes: jpegBytes, name: "photo.jpg" }); console.log(result.mime?.mimeType); // "image/jpeg" console.log(result.exif?.format); // "jpeg" ``` ## Usage: Advanced Example ```ts import { MetadataTransform, type EnrichedPayload, } from "@suaveplan/streams-metadata"; const transform = new MetadataTransform({ mode: "enrich", read: { skipXmp: true }, }); const events: string[] = []; transform.on("mime:detected", (r) => events.push(`mime:${r.source}`)); transform.on("exif:detected", (m) => events.push(`exif:${m.format}`)); transform.on("metadata:complete", (p: EnrichedPayload) => events.push(`complete:${p.bytes.byteLength}`), ); transform.on("metadata:error", (err) => { console.error("metadata pipeline failed", err); }); await transform.process({ bytes: jpegBytes, name: "photo.jpg" }); // events => ["mime:magic-bytes", "exif:jpeg", "complete:..."] ``` ## API Reference - `class MetadataTransform extends BaseTransform<{}, StreamsMetadataEvents>`: - `constructor(options?: MetadataTransformOptions)` — accepts `{ mode?, read? }`. `mode` is shared across both children; `read` is forwarded to the inner `ExifTransform`. - `async process(input: T, options?: ProcessOptions): Promise` — runs `mime.process(input) → exif.process(...)`. On success emits `metadata:complete` and returns the final enriched payload. On any child throw emits `metadata:error` with the thrown value and returns the original `input` reference unchanged. - `dispose(): void` — releases both child transforms and the wrapper's own listener / hook tables. Idempotent. - `name: "metadata"`. - `type MetadataTransformOptions = MimeTransformOptions & ExifTransformOptions` — shared `mode` flag plus EXIF-specific `read` options. ## Implementation Notes - Child events are wired in the constructor via `this.mime.on(...)` and `this.exif.on(...)`; the lambdas call `this.emit(...)` on the wrapper to re-emit. The `Unsubscribe` returns are dropped because the child transforms' listener tables are released through `this.mime.dispose()` / `this.exif.dispose()` in the override. - Error containment: a child throw never propagates out of `process` — observability via `metadata:error` is the canonical failure channel. The original `input` reference is returned (not a clone) so referentially-stable downstream stages keep working. - Subclasses overriding `dispose()` MUST call `super.dispose()` (per the base contract). This class follows that rule by chaining `super.dispose()` after disposing children. - The constructor honours `exactOptionalPropertyTypes` by only forwarding `mode` / `read` when explicitly supplied, so the children use their own defaults when the wrapper's options bundle omits the field.