Initial Commit
This commit is contained in:
206
packages/foundation/data/packer/src/abstract/abstract.ts
Normal file
206
packages/foundation/data/packer/src/abstract/abstract.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user