import { sha256 } from "@suaveplan/crypto-hash"; import { EventBus } from "@suaveplan/events"; import { getMimeType } from "@suaveplan/mime"; import type { MessageResolver } from "@suaveplan/types"; import { type PackerFeatureFlags, resolvePackerFlags } from "../flags/flags.js"; import { defaultPackerResolver } from "../i18n/messages.js"; import { PackIdempotencyGuard } from "../idempotency/idempotency.js"; import type { PackIdempotencyOptions } from "../idempotency/idempotency.types.js"; import { createPackerObservability, type PackerObservability, } from "../observability/observability.js"; import { PluginPipeline } from "../plugins/plugins.js"; import { retryRead } from "../retry/retryable-read.js"; import type { RetryConfig, RetryReadOptions, } from "../retry/retryable-read.types.js"; import { resolveRuntime } from "../runtime/runtime.js"; import type { PackerRuntime } from "../runtime/runtime.types.js"; import type { PackedData, PackedDataMetadata, PackerEventMap, PackerOptions, } from "../types/packer.types.js"; import type { PackSource } from "../types/source.types.js"; /** * Resolved (defaults-applied) options for {@link AbstractPacker}. * Internal — derived from the optional `PackerOptions` once at construction. */ export interface ResolvedPackerOptions { compressionLevel: number; encryption: PackerOptions["encryption"]; signing: PackerOptions["signing"]; metadata: Record; messageResolver: MessageResolver; observability: PackerObservability; runtime: PackerRuntime; idempotency: PackIdempotencyGuard | undefined; retryConfig: RetryConfig | undefined; flags: PackerFeatureFlags; plugins: PluginPipeline; } /** * Abstract base class for whole-file packer implementations. Carries the * resolved options bundle, a typed `EventBus` for observability * events, and a few shared helpers (checksum, MIME detection). * * Subclasses implement `pack` (and optionally `packStream`). */ export abstract class AbstractPacker extends EventBus { protected readonly options: ResolvedPackerOptions; public constructor(options: PackerOptions = {}) { super(); const messageResolver = options.messageResolver ?? defaultPackerResolver; const obsOpts: Parameters[0] = {}; if (options.observability !== undefined) obsOpts.observability = options.observability; if (options.meter !== undefined) obsOpts.meter = options.meter; if (options.tracer !== undefined) obsOpts.tracer = options.tracer; const observability = createPackerObservability(obsOpts); const runtime = resolveRuntime(options); const idempotency = makeIdempotency(options.idempotency, runtime); const flags = resolvePackerFlags(options.flags); const plugins = new PluginPipeline(options.plugins, messageResolver); this.options = { compressionLevel: options.compressionLevel ?? 6, encryption: options.encryption, signing: options.signing, metadata: { ...(options.metadata ?? {}) }, messageResolver, observability, runtime, idempotency, retryConfig: options.retryConfig, flags, plugins, }; } /** Pack `data` into a whole-file envelope. Subclasses define the pipeline. */ public abstract pack( data: Uint8Array, filenameOrOpts?: string | import("../types/packer.types.js").PackOptions ): Promise; /** * Pack a streaming source by collecting it into a buffer and delegating to * {@link pack}. Whole-file mode is not streaming-native; for true-streaming * use the chunked subpath. */ public async packStream( source: PackSource, filename?: string ): Promise { const buffer = await collect(source, { ...(this.options.retryConfig !== undefined ? { retryConfig: this.options.retryConfig } : {}), }); return this.pack(buffer, filename); } /** SHA-256 hex of `data`. Used for the `checksum` field of metadata. */ protected checksum(data: Uint8Array): string { return toHex(sha256(data)); } /** Detect MIME type from an optional filename via `@suaveplan/mime`. */ protected mimeFor(filename?: string): string | undefined { return filename ? getMimeType(filename) : undefined; } /** Build the metadata object common to all packed outputs. */ protected buildMetadata(input: { originalSize: number; compressedSize: number; encrypted: boolean; signed?: boolean; mimeType?: string; checksum: string; }): PackedDataMetadata { return { originalSize: input.originalSize, compressedSize: input.compressedSize, encrypted: input.encrypted, ...(input.signed !== undefined ? { signed: input.signed } : {}), ...(input.mimeType !== undefined ? { mimeType: input.mimeType } : {}), timestamp: this.options.runtime.clock(), checksum: input.checksum, ...(Object.keys(this.options.metadata).length > 0 ? { custom: { ...this.options.metadata } } : {}), }; } } function makeIdempotency( opts: PackIdempotencyOptions | undefined, runtime: PackerRuntime ): PackIdempotencyGuard | undefined { if (opts === undefined) return undefined; return new PackIdempotencyGuard(opts, runtime.clock); } /** Collect a streaming source into a single `Uint8Array`. */ async function collect( source: PackSource, opts: RetryReadOptions ): Promise { if (source instanceof Uint8Array) return source; if (typeof Blob !== "undefined" && source instanceof Blob) { return new Uint8Array(await source.arrayBuffer()); } const parts: Uint8Array[] = []; let total = 0; if ( source !== null && typeof source === "object" && Symbol.asyncIterator in source ) { for await (const part of source as AsyncIterable) { parts.push(part); total += part.byteLength; } } else { const reader = ( source as unknown as ReadableStream ).getReader(); try { for (;;) { const { value, done } = await retryRead(reader, opts); if (done) break; if (value) { parts.push(value); total += value.byteLength; } } } finally { reader.releaseLock(); } } const out = new Uint8Array(total); let off = 0; for (const p of parts) { out.set(p, off); off += p.byteLength; } return out; } function toHex(bytes: Uint8Array): string { let s = ""; for (let i = 0; i < bytes.byteLength; i++) { const b = bytes[i] ?? /* istanbul ignore next */ 0; s += b.toString(16).padStart(2, "0"); } return s; }