Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
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<string, unknown>;
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<PackerEventMap>` for observability
* events, and a few shared helpers (checksum, MIME detection).
*
* Subclasses implement `pack` (and optionally `packStream`).
*/
export abstract class AbstractPacker extends EventBus<PackerEventMap> {
protected readonly options: ResolvedPackerOptions;
public constructor(options: PackerOptions = {}) {
super();
const messageResolver =
options.messageResolver ?? defaultPackerResolver;
const obsOpts: Parameters<typeof createPackerObservability>[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<PackedData>;
/**
* 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<PackedData> {
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<Uint8Array> {
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<Uint8Array>) {
parts.push(part);
total += part.byteLength;
}
} else {
const reader = (
source as unknown as ReadableStream<Uint8Array>
).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;
}