import { createHash } from 'node:crypto'; import type { ArtifactDescriptor, AssetCodec, AssetStageError, CookProduct, Result, } from '@forgeax/engine-types'; import { err, ok } from '@forgeax/engine-types'; export interface NativeCookArtifact { readonly mediaType: string; readonly assetCodec?: AssetCodec; readonly bytes: Uint8Array; } export interface NativeCookDraft
{
readonly guid: string;
readonly payload: P;
readonly refs: readonly string[];
readonly artifacts: Readonly {
readonly key: string;
readonly discover?: (input: unknown) => I | Promise;
cook(input: I): NativeCookDraft | Promise {
readonly draft: NativeCookDraft ;
readonly generation: number;
}
export interface NativeCookTransactionOptions {
readonly key: string;
readonly input: I;
readonly previous?: NativeCookTransactionSnapshot ;
readonly validate?: (draft: NativeCookDraft ) => string | undefined;
readonly publish?: (draft: NativeCookDraft , generation: number) => void | Promise extends NativeCookTransactionSnapshot {
readonly status: 'committed' | 'recovered';
readonly lastKnownGood: NativeCookDraft ;
readonly candidateGeneration: number;
readonly lastKnownGoodGeneration: number;
readonly recoveryHint?: string;
}
function canonicalize(value: unknown): unknown {
if (value instanceof Uint8Array) return Buffer.from(value).toString('base64');
if (Array.isArray(value)) return value.map(canonicalize);
if (typeof value !== 'object' || value === null) return value;
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, nested]) => [key, canonicalize(nested)]),
);
}
function digestDraft (draft: NativeCookDraft ): string {
const bytes = JSON.stringify(
canonicalize({
guid: draft.guid,
payload: draft.payload,
refs: draft.refs,
artifacts: draft.artifacts,
}),
);
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
}
type NativeCookErrorDetail = Extract<
AssetStageError['detail'],
{ readonly guid: string; readonly producer: string }
>;
function failure(key: string, detail: NativeCookErrorDetail): Result (
key: string,
previous: NativeCookTransactionSnapshot ,
): NativeCookTransactionResult {
return {
...previous,
status: 'recovered',
lastKnownGood: previous.draft,
candidateGeneration: previous.generation + 1,
lastKnownGoodGeneration: previous.generation,
recoveryHint: `repair ${key} and submit a new candidate generation`,
};
}
/** Injectable build-time table for native authored Pack producers. */
export class NativeCookerRegistry {
private readonly cookers = new Map (
key: string,
input: I,
): Promise | undefined;
if (cooker === undefined) {
return failure(key, { guid: 'unknown', producer: key });
}
let draft: NativeCookDraft ;
try {
const typedInput = cooker.discover === undefined ? input : await cooker.discover(input);
draft = await cooker.cook(typedInput);
} catch (error) {
return failure(key, { guid: 'unknown', producer: thrownProducer(error) });
}
if (
draft.guid.length === 0 ||
draft.inputFingerprint.length === 0 ||
!Array.isArray(draft.refs) ||
typeof draft.payload !== 'object' ||
draft.payload === null ||
typeof draft.artifacts !== 'object' ||
draft.artifacts === null ||
Object.entries(draft.artifacts).some(
([, artifact]) =>
artifact === null ||
typeof artifact !== 'object' ||
typeof artifact.mediaType !== 'string' ||
artifact.mediaType.length === 0 ||
!(artifact.bytes instanceof Uint8Array),
)
) {
return failure(key, { guid: draft.guid || 'unknown', producer: key });
}
return ok(draft);
}
async run (
key: string,
input: I,
): Promise (key, input);
if (!draftResult.ok) return draftResult;
const draft = draftResult.value;
const digest = digestDraft(draft);
const artifacts: Record = {
guid: draft.guid,
payload: draft.payload,
refs: draft.refs,
artifacts,
digest,
receipt,
};
return ok(product);
}
async runTransaction (
options: NativeCookTransactionOptions ,
): Promise (options.key, options.input);
if (!candidate.ok) {
return options.previous === undefined
? candidate
: ok(recovered(options.key, options.previous));
}
const invalidReason = options.validate?.(candidate.value);
if (invalidReason !== undefined) {
return options.previous === undefined
? transactionFailure(options.key, invalidReason)
: ok(recovered(options.key, options.previous));
}
const generation = (options.previous?.generation ?? 0) + 1;
try {
await options.publish?.(candidate.value, generation);
} catch {
return options.previous === undefined
? transactionFailure(options.key, 'atomic producer publication failed')
: ok(recovered(options.key, options.previous));
}
return ok({
draft: candidate.value,
generation,
status: 'committed',
lastKnownGood: candidate.value,
candidateGeneration: generation,
lastKnownGoodGeneration: generation,
});
}
}