mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
fix(studio): enforce optimistic file concurrency (#2156)
* fix(studio): enforce optimistic file concurrency * fix(studio): harden conditional file writes * fix(studio): honor explicit file preconditions * test(producer): allow zero-ms encode timing
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
consumeFileWriteReceipt,
|
||||
fileContentVersion,
|
||||
recordFileWriteReceipt,
|
||||
resetFileWriteReceipts,
|
||||
} from "./fileVersion";
|
||||
|
||||
afterEach(resetFileWriteReceipts);
|
||||
|
||||
describe("file versions and write receipts", () => {
|
||||
it("produces a strong quoted SHA-256 ETag", () => {
|
||||
expect(fileContentVersion("abc")).toBe(
|
||||
'"sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"',
|
||||
);
|
||||
});
|
||||
|
||||
it("attaches each API write identity to exactly one watcher echo", () => {
|
||||
const receipt = {
|
||||
path: "index.html",
|
||||
version: fileContentVersion("after"),
|
||||
writeToken: "write-1",
|
||||
};
|
||||
recordFileWriteReceipt("/project/index.html", receipt);
|
||||
|
||||
expect(consumeFileWriteReceipt("/project/index.html")).toEqual(receipt);
|
||||
expect(consumeFileWriteReceipt("/project/index.html")).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
|
||||
export interface FileWriteReceipt {
|
||||
path: string;
|
||||
version: string;
|
||||
writeToken: string;
|
||||
}
|
||||
|
||||
interface StoredReceipt extends FileWriteReceipt {
|
||||
recordedAt: number;
|
||||
}
|
||||
|
||||
const RECEIPT_TTL_MS = 10_000;
|
||||
const receipts = new Map<string, StoredReceipt[]>();
|
||||
|
||||
/** Strong content version used as both the JSON version and HTTP ETag. */
|
||||
export function fileContentVersion(content: string): string {
|
||||
return `"sha256:${createHash("sha256").update(content, "utf8").digest("hex")}"`;
|
||||
}
|
||||
|
||||
export function createWriteToken(requestToken?: string): string {
|
||||
const token = requestToken?.trim();
|
||||
return token && token.length <= 200 ? token : randomUUID();
|
||||
}
|
||||
|
||||
export function recordFileWriteReceipt(absPath: string, receipt: FileWriteReceipt): void {
|
||||
const now = Date.now();
|
||||
const current = (receipts.get(absPath) ?? []).filter(
|
||||
(entry) => now - entry.recordedAt < RECEIPT_TTL_MS,
|
||||
);
|
||||
current.push({ ...receipt, recordedAt: now });
|
||||
receipts.set(absPath, current);
|
||||
}
|
||||
|
||||
/** Attach one API write's identity to the corresponding filesystem-watch echo. */
|
||||
export function consumeFileWriteReceipt(absPath: string): FileWriteReceipt | null {
|
||||
const now = Date.now();
|
||||
const current = (receipts.get(absPath) ?? []).filter(
|
||||
(entry) => now - entry.recordedAt < RECEIPT_TTL_MS,
|
||||
);
|
||||
const receipt = current.shift() ?? null;
|
||||
if (current.length > 0) receipts.set(absPath, current);
|
||||
else receipts.delete(absPath);
|
||||
if (!receipt) return null;
|
||||
const { path, version, writeToken } = receipt;
|
||||
return { path, version, writeToken };
|
||||
}
|
||||
|
||||
export function resetFileWriteReceipts(): void {
|
||||
receipts.clear();
|
||||
}
|
||||
Reference in New Issue
Block a user