This commit is contained in:
Your Name committed 2026-07-25 03:02:07 -04:00
1 parent 9a915ee395
commit f64fee1347
2 files changed
+249 -66

No files matched your search

+106 -4
View File
@@ -9,8 +9,11 @@ import {
isValidMenuUrl,
lockMenuForTransaction,
lockMenuNameForTransaction,
MenuBusyError,
menusRoute,
prepareMenuForAdminWrite,
resolveMenuItemLabel,
runAddMenuTransaction,
} from "./menus";
const menu = {
@@ -135,11 +138,11 @@ describe("footer menu helpers", () => {
$queryRaw: async (strings: TemplateStringsArray, ...parameters: unknown[]) => {
query = strings.join("?");
values = parameters;
return [];
return [{ locked: true }];
},
}, menu.id);
expect(query).toBe("SELECT pg_advisory_xact_lock(?)");
expect(query).toBe("SELECT pg_try_advisory_xact_lock(?) AS locked");
expect(values).toEqual([menu.id]);
});
@@ -150,14 +153,113 @@ describe("footer menu helpers", () => {
$queryRaw: async (strings: TemplateStringsArray, ...parameters: unknown[]) => {
query = strings.join("?");
values = parameters;
return [];
return [{ locked: true }];
},
}, "footer-explore");
expect(query).toBe("SELECT pg_advisory_xact_lock(1213025877, hashtext(?))");
expect(query).toBe("SELECT pg_try_advisory_xact_lock(1213025877, hashtext(?)) AS locked");
expect(values).toEqual(["footer-explore"]);
});
test("rejects a false name lock without invoking subsequent work", async () => {
let writes = 0;
const operation = async () => {
await lockMenuNameForTransaction({
$queryRaw: async () => [{ locked: false }],
}, "footer-explore");
writes += 1;
};
await expect(operation()).rejects.toBeInstanceOf(MenuBusyError);
expect(writes).toBe(0);
});
test("rejects a false ID lock so an enclosing bootstrap transaction can rollback", async () => {
let writes = 0;
const operation = async () => {
await lockMenuForTransaction({
$queryRaw: async () => [{ locked: false }],
}, menu.id);
writes += 1;
};
await expect(operation()).rejects.toBeInstanceOf(MenuBusyError);
expect(writes).toBe(0);
});
test("rolls back a bootstrapped menu when the ID lock is busy", async () => {
let persistedMenu = false;
let writes = 0;
let lockCalls = 0;
const db = {
$transaction: async (callback: (tx: any) => Promise<unknown>) => {
let transactionCreatedMenu = false;
const tx = {
$queryRaw: async () => {
lockCalls += 1;
return [{ locked: lockCalls === 1 }];
},
menu: {
findUnique: async () => null,
create: async () => {
transactionCreatedMenu = true;
return { id: 99 };
},
},
};
try {
const result = await callback(tx);
persistedMenu = transactionCreatedMenu;
return result;
} catch (error) {
persistedMenu = false;
throw error;
}
},
};
const result = await runAddMenuTransaction(
"footer-explore",
null,
async () => {
writes += 1;
},
db,
);
expect(result).toEqual({ busy: true });
expect(lockCalls).toBe(2);
expect(persistedMenu).toBe(false);
expect(writes).toBe(0);
});
test("does not update existing Admin menu metadata when the ID lock is busy", async () => {
let lockCalls = 0;
let updates = 0;
const tx = {
$queryRaw: async () => {
lockCalls += 1;
return [{ locked: lockCalls === 1 }];
},
menu: {
findUnique: async () => ({ id: menu.id }),
create: async () => {
throw new Error("Existing menu must not be created.");
},
update: async () => {
updates += 1;
return { id: menu.id };
},
},
};
await expect(
prepareMenuForAdminWrite(tx, menu.name, { isActive: false }),
).rejects.toBeInstanceOf(MenuBusyError);
expect(lockCalls).toBe(2);
expect(updates).toBe(0);
});
test("returns the compact empty read contract for an absent footer menu", () => {
expect(compactMissingFooterMenu("footer-support", "zh")).toEqual({
name: "footer-support",
+143 -62
View File
@@ -111,12 +111,62 @@ const getFooterMenu = async (
include: footerMenuInclude,
});
export class MenuBusyError extends Error {
constructor() {
super("Menu is currently being edited.");
this.name = "MenuBusyError";
}
}
const assertAdvisoryLock = (rows: unknown) => {
if (
!Array.isArray(rows)
|| rows.length === 0
|| (rows[0] as { locked?: unknown }).locked !== true
) {
throw new MenuBusyError();
}
};
export const lockMenuForTransaction = async (tx: any, menuId: number) => {
await tx.$queryRaw`SELECT pg_advisory_xact_lock(${menuId})`;
const rows = await tx.$queryRaw`SELECT pg_try_advisory_xact_lock(${menuId}) AS locked`;
assertAdvisoryLock(rows);
return true;
};
export const lockMenuNameForTransaction = async (tx: any, name: FooterMenuName | string) => {
await tx.$queryRaw`SELECT pg_advisory_xact_lock(1213025877, hashtext(${name}))`;
const rows = await tx.$queryRaw`SELECT pg_try_advisory_xact_lock(1213025877, hashtext(${name})) AS locked`;
assertAdvisoryLock(rows);
return true;
};
export const prepareMenuForAdminWrite = async (
tx: any,
name: string,
updateData: Record<string, unknown>,
) => {
await lockMenuNameForTransaction(tx, name);
let identity = await tx.menu.findUnique({
where: { name },
select: { id: true },
});
if (!identity) {
identity = await tx.menu.create({
data: { name },
select: { id: true },
});
}
await lockMenuForTransaction(tx, identity.id);
let menu = await tx.menu.findUnique({ where: { name } });
if (!menu) throw new Error(`Menu ${name} disappeared before Admin write.`);
if (Object.keys(updateData).length > 0) {
menu = await tx.menu.update({
where: { name },
data: updateData,
});
}
return menu;
};
export async function executeVersionedMenuWrite<T>(
@@ -162,72 +212,95 @@ const runMenuTransaction = async <T>(
name: FooterMenuName,
expectedVersion: unknown,
write: (tx: any, menu: VersionedMenu) => Promise<T>,
) =>
prisma.$transaction(async (tx: any) => {
await lockMenuNameForTransaction(tx, name);
const identity = await tx.menu.findUnique({
where: { name },
select: { id: true },
});
if (!identity) return { missing: true as const };
await lockMenuForTransaction(tx, identity.id);
) => {
try {
return await prisma.$transaction(async (tx: any) => {
await lockMenuNameForTransaction(tx, name);
const identity = await tx.menu.findUnique({
where: { name },
select: { id: true },
});
if (!identity) return { missing: true as const };
await lockMenuForTransaction(tx, identity.id);
const menu = await getFooterMenu(name, tx);
if (!menu) return { missing: true as const };
return executeVersionedMenuWrite(menu, expectedVersion, async () => {
const value = await write(tx, menu);
const updated = await getFooterMenu(name, tx);
if (!updated) throw new Error(`Menu ${name} disappeared during transaction.`);
return { value, version: fingerprintMenu(updated) };
const menu = await getFooterMenu(name, tx);
if (!menu) return { missing: true as const };
return executeVersionedMenuWrite(menu, expectedVersion, async () => {
const value = await write(tx, menu);
const updated = await getFooterMenu(name, tx);
if (!updated) throw new Error(`Menu ${name} disappeared during transaction.`);
return { value, version: fingerprintMenu(updated) };
});
});
});
} catch (error) {
if (error instanceof MenuBusyError) return { busy: true as const };
throw error;
}
};
const runAddMenuTransaction = async (
export const runAddMenuTransaction = async (
name: FooterMenuName,
expectedVersion: unknown,
write: (tx: any, menu: VersionedMenu) => Promise<unknown>,
) =>
prisma.$transaction(async (tx: any) => {
await lockMenuNameForTransaction(tx, name);
let identity = await tx.menu.findUnique({
where: { name },
select: { id: true },
});
const createdMenu = !identity;
if (!identity) {
if (expectedVersion !== undefined && expectedVersion !== null) {
return { written: false as const, currentVersion: null };
}
identity = await tx.menu.create({
data: { name, isActive: true },
db: any = prisma,
) => {
try {
return await db.$transaction(async (tx: any) => {
await lockMenuNameForTransaction(tx, name);
let identity = await tx.menu.findUnique({
where: { name },
select: { id: true },
});
}
const createdMenu = !identity;
if (!identity) {
if (expectedVersion !== undefined && expectedVersion !== null) {
return { written: false as const, currentVersion: null };
}
identity = await tx.menu.create({
data: { name, isActive: true },
select: { id: true },
});
}
await lockMenuForTransaction(tx, identity.id);
const menu = await getFooterMenu(name, tx);
if (!menu) throw new Error(`Menu ${name} disappeared during bootstrap transaction.`);
return executeVersionedMenuWrite(
menu,
createdMenu ? undefined : expectedVersion,
async () => {
const value = await write(tx, menu);
const updated = await getFooterMenu(name, tx);
if (!updated) throw new Error(`Menu ${name} disappeared during add transaction.`);
return {
value,
version: fingerprintMenu(updated),
createdMenu,
};
},
);
});
await lockMenuForTransaction(tx, identity.id);
const menu = await getFooterMenu(name, tx);
if (!menu) throw new Error(`Menu ${name} disappeared during bootstrap transaction.`);
return executeVersionedMenuWrite(
menu,
createdMenu ? undefined : expectedVersion,
async () => {
const value = await write(tx, menu);
const updated = await getFooterMenu(name, tx);
if (!updated) throw new Error(`Menu ${name} disappeared during add transaction.`);
return {
value,
version: fingerprintMenu(updated),
createdMenu,
};
},
);
});
} catch (error) {
if (error instanceof MenuBusyError) return { busy: true as const };
throw error;
}
};
const unwrapTransaction = (
result: Awaited<ReturnType<typeof runMenuTransaction>>,
name: FooterMenuName,
set: { status?: number | string },
) => {
if ("busy" in result) {
return {
error: errorResponse(
set,
409,
"MENU_BUSY",
"Menu is currently being edited. Read it again and retry.",
),
};
}
if ("missing" in result) {
return { error: errorResponse(set, 404, "MENU_NOT_FOUND", `Menu ${name} was not found.`) };
}
@@ -410,6 +483,14 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
include: { translations: true },
}),
);
if ("busy" in transaction) {
return errorResponse(
set,
409,
"MENU_BUSY",
"Menu is currently being edited. Read it again and retry.",
);
}
if (!transaction.written) {
return errorResponse(
set,
@@ -732,15 +813,7 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
if (rootImage !== undefined) updateData.rootImage = rootImage;
return await prisma.$transaction(async (tx: any) => {
await lockMenuNameForTransaction(tx, name);
let menu = await tx.menu.findUnique({ where: { name } });
if (!menu) {
menu = await tx.menu.create({ data: { name, ...updateData } });
} else if (Object.keys(updateData).length > 0) {
menu = await tx.menu.update({ where: { name }, data: updateData });
}
await lockMenuForTransaction(tx, menu.id);
const menu = await prepareMenuForAdminWrite(tx, name, updateData);
const existingItems = await tx.menuItem.findMany({ where: { menuId: menu.id } });
const existingItemIds = existingItems.map((i: any) => i.id);
const payloadItemIds = items.map(i => i.id).filter(id => id != null);
@@ -796,6 +869,14 @@ export const menusRoute = new Elysia({ prefix: "/menus" })
});
});
} catch (err) {
if (err instanceof MenuBusyError) {
return errorResponse(
set,
409,
"MENU_BUSY",
"Menu is currently being edited. Read it again and retry.",
);
}
console.error(err);
set.status = 500;
return { error: "Failed to update menu" };