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

+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" };