Files
2026-05-27 23:29:53 +01:00

117 lines
5.1 KiB
Markdown

# Design: enterprise-media
## Module layout
```
packages/foundation/data/media/src/
detection/ detectMediaType, detectSignature, detectMediaTypeFromName,
inferMimeFromSignature, defaultMimeForType
metadata/ getMetadata, getMetadataAsync, MetadataOptions
validation/ validateMedia, validateMediaAsync, assertValidMedia,
ValidateOptions
signatures/ BUILTIN_SIGNATURES, SignatureRegistry,
defaultSignatureRegistry
errors/ MEDIA_CODES, MEDIA_ERROR_METADATA, MediaError, asMediaError
observability/ createMediaObservability + counter / histogram types
types/ MediaType, MediaSignature, MediaMetadata, MediaConstraints,
MediaResult, MediaMetadataExtractor + Zod schemas
index.ts root barrel — explicit named re-exports
```
Every feature folder is the canonical four-file pattern (`<name>.ts`,
`<name>.test.ts`, `<name>.md`, `index.ts`). Co-located `.md` docs cover
purpose, features, basic + advanced usage, API reference, and
implementation notes.
## Detection pipeline
1. `detectMediaType(input, opts?)` resolves `opts.registry ??
defaultSignatureRegistry` and delegates to `registry.detect(input)`.
2. `detectSignature(input, opts?)` returns the matched
`MediaSignature | null` so consumers can route on the label
(PNG / JPEG / MP4 / …) and not just the coarse `MediaType`.
3. `detectMediaTypeFromName(filename, opts?)` calls
`@suaveplan/mime.getMimeType` and buckets the result by top-level
type (`image/`, `video/`, `audio/`).
4. `inferMimeFromSignature(sig)` maps the matched signature label to
a canonical MIME, falling back to `defaultMimeForType(sig.type)`
when no per-label mapping exists.
`SignatureRegistry` exposes `register`, `unregister`, `list`, `match`,
and `detect`. `register` rejects empty byte arrays
(`MEDIA.INVALID_SIGNATURE`), non-byte values
(`MEDIA.INVALID_SIGNATURE`), and duplicate labels
(`MEDIA.DUPLICATE_SIGNATURE`). The built-in list orders longer / more-
specific signatures first so the first match wins.
## Metadata pipeline
`getMetadata` builds the base bundle (`{ type, mimeType, size,
signatureLabel }`) in a private helper. `getMetadataAsync` accepts an
optional `MediaMetadataExtractor` that receives `(input, base)` and
returns `Partial<MediaMetadata>`; the helper merges the partial over the
base. Extractor exceptions are wrapped in
`MediaError(MEDIA.METADATA_EXTRACTOR_FAILED)` with the original error
chained.
MIME resolution prefers the explicit `filename` field (via
`getMimeType`) and falls back to `inferMimeFromSignature`. The base
bundle exposes `signatureLabel` so downstream extractors can route on
the exact format.
## Validation pipeline
`validateMedia` runs:
1. `MediaConstraintsSchema.safeParse(constraints)` (unless caller passes
`skipConstraintValidation: true`). Failure raises
`MediaError(MEDIA.INVALID_CONSTRAINTS)`.
2. Empty-input guard: zero-byte input raises
`MediaError(MEDIA.EMPTY_INPUT)`.
3. `getMetadata(input, opts)` to build the bundle.
4. `checkConstraints(metadata, constraints)` to populate the error array
(size floors / ceilings, type / MIME allowlists, width / height
ceilings + floors, duration ceiling).
`validateMediaAsync` runs the same pipeline but calls `getMetadataAsync`
and, after constraint checks, runs `constraints.predicate(metadata,
input)`. Predicate return values:
- `null` / `undefined` — pass.
- `string` — push onto the errors array.
- thrown — wrapped in `MediaError(MEDIA.PREDICATE_FAILED)`.
`assertValidMedia` calls the sync variant and throws
`MediaError(MEDIA.VALIDATION_FAILED)` if any error is reported,
incrementing the `media_error_total` counter with `{ code }`.
## Observability
`createMediaObservability(opts?)` builds the bundle. If
`opts.observability` is supplied it short-circuits. Otherwise the
function resolves `meter` and `tracer` from `opts` or the global
`@opentelemetry/api` providers and constructs counters / histograms via
the helpers in `@suaveplan/opentelemetry`. Every public entrypoint
constructs a default bundle when none is supplied so calls always emit
telemetry without requiring caller-side wiring.
## Error model
`MEDIA_CODES` is built via `defineCodes("MEDIA", [...] as const)` so the
namespace and value strings line up with the rest of the monorepo.
`MEDIA_ERROR_METADATA` is `Object.freeze`'d at module load.
`MediaError` extends `SuaveplanError<MediaErrorCode>` and exposes a
`.metadata` getter that pulls the matching record. `asMediaError(code,
cause, fallback)` lifts unknown thrown values, preserving the original
error as `.cause` when it's already an `Error` instance.
## Why split into seven feature folders
The package is small (~700 LOC after the enterprise rewrite) but each
module owns a distinct concern: detection / metadata / validation are
end-user surfaces; signatures / errors / observability are
infrastructure shared across the three; types holds the data contracts.
The split keeps `<file>.ts` under the 300-line hard limit and lets
tree-shake-sensitive consumers deep-import via the subpath exports
without dragging in the full bundle.