Files
tech-log-frontend/scripts/lib/validated-json-artifact.ts
T

145 lines
4.3 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { constants } from "node:fs";
import {
open as openFile,
rename as renameFile,
rm as removeFile,
} from "node:fs/promises";
import path from "node:path";
import type { z } from "zod";
export type ValidatedJsonArtifactInput = Readonly<{
path: string;
schema: z.ZodType;
value: unknown;
}>;
export function serializeValidatedJsonArtifact(
input: ValidatedJsonArtifactInput,
): Buffer {
const parsed = input.schema.parse(input.value);
const serialized = JSON.stringify(parsed, null, 2);
if (serialized === undefined) {
throw new TypeError("Validated JSON artifact is not serializable");
}
return Buffer.from(`${serialized}\n`, "utf8");
}
export type ValidatedJsonArtifactFileSystem = Readonly<{
open: (path: string, flags: number, mode: number) => Promise<{
writeFile(data: string, encoding: "utf8"): Promise<unknown>;
sync(): Promise<unknown>;
close(): Promise<unknown>;
}>;
openDirectory: (path: string) => Promise<{
sync(): Promise<unknown>;
close(): Promise<unknown>;
}>;
rename: (source: string, destination: string) => Promise<unknown>;
rm: (path: string, options: Readonly<{ force: true }>) => Promise<unknown>;
}>;
type ValidatedJsonArtifactWriterDependencies = Readonly<{
createNonce?: () => string;
fileSystem?: ValidatedJsonArtifactFileSystem;
}>;
const defaultFileSystem: ValidatedJsonArtifactFileSystem = Object.freeze({
open: async (target, flags, mode) => openFile(target, flags, mode),
openDirectory: async (target) => openFile(target, constants.O_RDONLY),
rename: async (source, destination) => renameFile(source, destination),
rm: async (target, options) => removeFile(target, options),
});
function hasErrorCode(error: unknown, code: string): boolean {
return (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === code
);
}
/**
* Builds a writer whose only publish operation is an atomic sibling rename.
* Dependency injection is limited to the file-system boundary so failure
* ownership can be verified without exposing a caller-selected cleanup path.
*/
export function createValidatedJsonArtifactWriter(
dependencies: ValidatedJsonArtifactWriterDependencies = {},
) {
const createNonce = dependencies.createNonce ?? randomUUID;
const fileSystem = dependencies.fileSystem ?? defaultFileSystem;
return async function writeArtifact(
input: ValidatedJsonArtifactInput,
): Promise<void> {
const serialized = serializeValidatedJsonArtifact(input);
const temporaryPath = path.join(
path.dirname(input.path),
`.${path.basename(input.path)}.${createNonce()}.tmp`,
);
let ownsTemporaryFile = false;
try {
const handle = await fileSystem.open(
temporaryPath,
constants.O_WRONLY |
constants.O_CREAT |
constants.O_EXCL |
constants.O_NOFOLLOW,
0o600,
);
ownsTemporaryFile = true;
let writeFailed = false;
let writeFailure: unknown;
try {
await handle.writeFile(serialized.toString("utf8"), "utf8");
await handle.sync();
} catch (error) {
writeFailed = true;
writeFailure = error;
}
let closeFailed = false;
let closeFailure: unknown;
try {
await handle.close();
} catch (error) {
closeFailed = true;
closeFailure = error;
}
if (writeFailed) throw writeFailure;
if (closeFailed) throw closeFailure;
await fileSystem.rename(temporaryPath, input.path);
ownsTemporaryFile = false;
const directoryHandle = await fileSystem.openDirectory(
path.dirname(input.path),
);
try {
try {
await directoryHandle.sync();
} catch (error) {
if (!hasErrorCode(error, "EINVAL") && !hasErrorCode(error, "ENOTSUP")) {
throw error;
}
}
} finally {
await directoryHandle.close();
}
} catch (error) {
if (ownsTemporaryFile) {
try {
await fileSystem.rm(temporaryPath, { force: true });
} catch {
// Preserve the publishing failure; cleanup is confined to our nonce.
}
}
throw error;
}
};
}
export const writeValidatedJsonArtifact =
createValidatedJsonArtifactWriter();