the .part-then-rename rule was applied at two upload routes yesterday and by this morning the platform had a dozen other writes putting files in the same folders — an OCR result beside its image, a TTS clip, a download landing, a save from the editor — every one straight to the final name. a guarantee that holds for two of fourteen paths is not a guarantee, and the next person adding a fifteenth had nothing to copy. so it is a function now, and every durable write goes through it: the file browser, sendoff, task definitions, skill and tool files, bug reports, scraped attachments, the chat provider config. the failure it prevents is silent rather than loud. a truncated m4a decodes fine, it is simply short — a commander transcribing recordings as they land would produce a clean transcript of the first few seconds of a two-minute memo and report success. two things learned writing it. Bun.write creates the parent chain itself, so the mkdir option i added was dead weight and several callers were doing it twice. and Parameters<typeof Bun.write>[1] resolves to the first overload only — BunFile — which rejects the string and Buffer every caller actually passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
52 lines
2.8 KiB
TypeScript
52 lines
2.8 KiB
TypeScript
import { rename, rm } from 'node:fs/promises';
|
|
|
|
/**
|
|
* Write a file so that its final name never holds a partial one.
|
|
*
|
|
* ── Why every write into a user's tree has to do this ──
|
|
*
|
|
* `Bun.write(path, data)` streams to the name the file will end up with, so for as long as the write
|
|
* takes, that name exists holding a fragment — and anything reading the folder cannot tell. The failure
|
|
* is silent rather than loud: a truncated m4a decodes fine, it is simply short. A commander transcribing
|
|
* recordings as they land would produce a clean transcript of the first few seconds of a two-minute
|
|
* recording and report success.
|
|
*
|
|
* A rename within one directory is atomic, so afterwards the final name either does not exist or is a
|
|
* complete file. Every consumer gets that for free — a watcher, an agent, the phone's own upload
|
|
* verification — instead of each inventing a heuristic like "the size stopped changing", which cannot
|
|
* tell a finished write from a stalled one.
|
|
*
|
|
* ── Why it is a function rather than a rule ──
|
|
*
|
|
* It was a rule, applied at the two upload routes on 2026-08-24, and by the next morning the platform
|
|
* had a dozen other writes that put files in the same folders — an OCR result beside its image, a
|
|
* download landing, a save from the editor — every one of them straight to the final name. A guarantee
|
|
* that holds for two of fourteen paths is not a guarantee, and the next person to add a fifteenth would
|
|
* have had nothing to copy.
|
|
*
|
|
* ── `.part`, visibly ──
|
|
*
|
|
* A leftover should be VISIBLE, named for what it was going to be, next to where it belongs — not a
|
|
* dotfile. Nothing sweeps them up: a job deleting files it did not create is a worse trade than a
|
|
* fragment somebody can see. Only an interrupted PROCESS leaves one, since a failed write cleans up
|
|
* after itself below.
|
|
*/
|
|
/** What `Bun.write` accepts, spelled out — `Parameters<typeof Bun.write>[1]` resolves to the first
|
|
* overload only, which is `BunFile` and rejects the string and Buffer every caller actually passes. */
|
|
type WritableData = string | Blob | ArrayBufferView | ArrayBufferLike | Response;
|
|
|
|
export async function writeAtomic(path: string, data: WritableData): Promise<void> {
|
|
// No mkdir: `Bun.write` creates the parent chain itself (`createPath` defaults on), which is why the
|
|
// callers this replaced could drop theirs too.
|
|
const partPath = `${path}.part`;
|
|
try {
|
|
// Bun.write's overloads do not union cleanly — every one of them accepts this at runtime, and the
|
|
// signature is a set of alternatives rather than one parameter that takes them all.
|
|
await Bun.write(partPath, data as Parameters<typeof Bun.write>[1]);
|
|
await rename(partPath, path);
|
|
} catch (err) {
|
|
await rm(partPath, { force: true }).catch(() => {});
|
|
throw err;
|
|
}
|
|
}
|