Integrate worker to offload main thread from heavy tasks

This commit is contained in:
2025-11-11 15:13:38 +01:00
parent 0f1fff0609
commit 0451a1c960
8 changed files with 115 additions and 205 deletions

17
bot.js
View File

@@ -1,7 +1,7 @@
import {data} from "./wwwroot/core/appData.js"; import {data} from "./wwwroot/core/appData.js";
import JsonManager from "./wwwroot/core/utils/jsonManager.js"; import JsonManager from "./wwwroot/core/utils/jsonManager.js";
import {Logger} from "./wwwroot/core/logging/logger.js"; import {Logger} from "./wwwroot/core/logging/logger.js";
import {NameCardCreator} from "./wwwroot/core/welcome/nameCardCreator.js"; import {launchWorker} from "./wwwroot/core/namecards/workerLauncher.js";
const launch = async () => { const launch = async () => {
try{ try{
@@ -30,8 +30,19 @@ const launch = async () => {
} }
}); });
data.client.on('guildMemberAdd', member => { data.client.on('guildMemberAdd', async member => {
NameCardCreator.getWelcomeCard("", member.user.avatarURL(), member.user.displayName) try {
launchWorker({
templatePath : data.nameCardTemplate,
avatarURL: member.user.avatarURL,
displayName: member.user.displayName,
}).then(buffer => {
//Send name card here...
console.log('Name card sent!');
}).catch(console.error);
} catch (err) {
console.error('Failed to generate/send name card:', err);
}
}); });
process.on('SIGINT', async () => { process.on('SIGINT', async () => {

View File

@@ -1,7 +1,21 @@
import {NameCardCreator} from "../wwwroot/core/welcome/nameCardCreator-2.js"; import {NameCardCreator} from "../wwwroot/core/namecards/nameCardCreator.js";
import {launchWorker} from "../wwwroot/core/namecards/workerLauncher.js";
import {data} from "../wwwroot/core/appData.js";
const templatePath = "./wwwroot/assets/name-card-template.png"; const templatePath = "./wwwroot/assets/name-card-template.png";
const avatarPath = "./tests/assets/avatar-test.png"; const avatarPath = "./tests/assets/avatar-test.png";
const name = "Aude Vaiselle"; const name = "Aude Vaiselle";
const creator = new NameCardCreator(templatePath);
await creator.getWelcomeCard(avatarPath, name)
try {
launchWorker({
templatePath : data.nameCardTemplate,
avatarURL: avatarPath,
username: name,
}).then(buffer => {
//Send name card here...
console.log('Name card sent!');
}).catch(console.error);
} catch (err) {
console.error('Failed to generate/send name card:', err);
}

View File

@@ -2,8 +2,6 @@ import 'dotenv/config';
import {Client, GatewayIntentBits} from "discord.js"; import {Client, GatewayIntentBits} from "discord.js";
import {MessageSender} from "./base/MessageSender.js"; import {MessageSender} from "./base/MessageSender.js";
import {InstagramTokenManager} from "./instagram/instagramTokenManager.js"; import {InstagramTokenManager} from "./instagram/instagramTokenManager.js";
import {TikTokTokenManager} from "./tiktok/tiktokTokenManager.js";
import JsonManager from "./utils/jsonManager.js";
import {InstagramPoller} from "./instagram/instagramPoller.js"; import {InstagramPoller} from "./instagram/instagramPoller.js";
import {UsersToken} from "./usersToken.js"; import {UsersToken} from "./usersToken.js";
@@ -16,6 +14,7 @@ const client = new Client({
] ]
}); });
const nameCardTemplate = "./wwwroot/assets/name-card-template.png";
const sender = new MessageSender(client); const sender = new MessageSender(client);
const instagramID = process.env.INSTAGRAM_CLIENT_ID; const instagramID = process.env.INSTAGRAM_CLIENT_ID;
@@ -41,6 +40,7 @@ export const data = {
updateChannelID, updateChannelID,
socialChannelID, socialChannelID,
instagramTokenManager, instagramTokenManager,
nameCardTemplate
//tiktokTokenManager //tiktokTokenManager
}; };

View File

@@ -30,6 +30,7 @@ export class Logger{
await mkdir(logsDir, { recursive: true }); await mkdir(logsDir, { recursive: true });
await appendFile(fullPath, `${time} - ${message} -> ${error.message} \n`); await appendFile(fullPath, `${time} - ${message} -> ${error.message} \n`);
console.error(error.stackTrace);
console.error(`An error occured. The incident has been logged in ${fullPath}`) console.error(`An error occured. The incident has been logged in ${fullPath}`)
} catch (err) { } catch (err) {
console.error("Error writing log:", err); console.error("Error writing log:", err);

View File

@@ -8,12 +8,12 @@ export class NameCardCreator {
this.loadFont("./wwwroot/assets/fonts/Fredoka/static/Fredoka-Bold.ttf") this.loadFont("./wwwroot/assets/fonts/Fredoka/static/Fredoka-Bold.ttf")
} }
loadFont(fontPath){ loadFont(fontPath) {
const fullPath = path.resolve(fontPath); const fullPath = path.resolve(fontPath);
if (!fs.existsSync(fullPath)) { if (!fs.existsSync(fullPath)) {
throw new Error(`Font file not found at ${fullPath}`); throw new Error(`Font file not found at ${fullPath}`);
} }
registerFont(fullPath, { family: "Fredoka Bold" }); registerFont(fullPath, {family: "Fredoka Bold"});
} }
/** /**
@@ -78,13 +78,31 @@ export class NameCardCreator {
ctx.fillText(line, messageX, startY + i * lineHeight); ctx.fillText(line, messageX, startY + i * lineHeight);
}); });
const buffer = canvas.toBuffer("image/png"); const result = this.scallDown(canvas, 1500, 500)
fs.writeFileSync("./tests/result/namecard.png", buffer);
fs.writeFileSync("./tests/result/namecard.png", result);
console.log("✅ Name card created: namecard.png"); console.log("✅ Name card created: namecard.png");
return buffer; return result;
} catch (err) { } catch (err) {
console.error("Error creating name card:", err); console.error("Error creating name card:", err);
throw err; throw err;
} }
} }
/**
*
* @param target
* @param width
* @param height
* @returns {Buffer}
*/
scallDown(target, width, height) {
const smallCanvas = createCanvas(width, height);
const smallCtx = smallCanvas.getContext('2d');
smallCtx.drawImage(target, 0, 0, width, height);
return smallCanvas.toBuffer();
}
} }

View File

@@ -0,0 +1,27 @@
import { parentPort } from 'worker_threads';
import {NameCardCreator} from "./nameCardCreator.js";
parentPort.on("message", async (data) => {
try {
const { templatePath, avatarURL, username } = data;
const creator = new NameCardCreator(templatePath);
const buffer = await creator.getWelcomeCard(avatarURL, username);
parentPort.postMessage({ result: buffer });
} catch (err) {
parentPort.postMessage({ error: err.message });
}
});
/*(async () => {
try {
const { creator, avatarURL, userName } = workerData;
const buffer = await creator.getWelcomeCard(avatarURL, userName);
parentPort.postMessage(buffer); // return the buffer to main thread
} catch (err) {
parentPort.postMessage({ error: err.message });
}
})();*/

View File

@@ -0,0 +1,31 @@
import path from "path";
import { Worker } from 'worker_threads';
import {Logger} from "../logging/logger.js";
function launchWorker({templatePath, avatarURL, username}) {
return new Promise((resolve, reject) => {
const workerFile = path.resolve('./wwwroot/core/namecards/nameCardWorker.js');
const worker = new Worker(workerFile);
worker.postMessage({templatePath, avatarURL, username});
worker.on('message', (result) => {
resolve(result);
worker.terminate()
.catch(err => Logger.error("Unable to terminate worker", err.message));
});
worker.on('error', (err) => {
reject(err);
worker.terminate()
.catch(err => Logger.error("Unable to terminate worker", err.message));
});
worker.on('exit', (code) => {
if (code !== 0) reject(new Error(`Worker stopped with code ${code}`));
});
});
}
export {launchWorker}

View File

@@ -1,192 +0,0 @@
import sharp from "sharp";
import {Logger} from "../logging/logger.js";
import fs from "fs";
import path from "path";
import { createCanvas, registerFont } from 'canvas';
export class NameCardCreator {
constructor(templatePath) {
this.templatePath = templatePath;
this.template = this.loadTemplate();
}
/**
* Loads the template file into a sharp instance
* @returns {sharp.Sharp} sharp image object
*/
loadTemplate() {
return sharp(this.templatePath);
}
loadFont(fontPath) {
const fullPath = path.resolve(fontPath);
console.debug(fullPath);
registerFont(fullPath, { family: 'Fredoka' });
}
/**
* Combines a template image with a user avatar and saves it
* @param avatarPath {string}
* @param name {string}
* @returns {Promise<sharp.OutputInfo>} resulting image buffer
*/
async getWelcomeCard(avatarPath, name) {
try{
const avatarSize = 550;
const avatar = await this.handleAvatar(avatarPath, avatarSize);
const messageWidth = 2000;
const messageHeight = 700;
const messageBuffer = await this.getMessageBuffer(name, messageWidth, messageHeight);
const avatarPos = { x : 150, y : (1000-avatarSize+16)/2}
const messagePos = { x : avatarPos.x + 700, y : (1000-messageHeight)/2}
const result = await this.template
.composite([
{ input: avatar, top: avatarPos.y, left: avatarPos.x },
{ input: messageBuffer, top: messagePos.y, left: messagePos.x }
])
.toFile("namecard-.png")
console.log("✅ Welcome card created: welcome-card.png");
return result;
} catch(err) {
console.log(err);
await Logger.error("Unable to create name card", err);
}
}
/**
*
* @param avatarPath
* @param outputSize
* @returns {Promise<Buffer<ArrayBufferLike>>}
*/
async handleAvatar(avatarPath, outputSize) {
const avatarSize = outputSize;
const borderSize = 8;
const totalSize = avatarSize + borderSize * 2;
const avatarRadius = avatarSize / 2;
const maskBuffer = Buffer.from(`
<svg width="${avatarSize}" height="${avatarSize}" xmlns="http://www.w3.org/2000/svg">
<circle cx="${avatarRadius}" cy="${avatarRadius}" r="${avatarRadius}" fill="white"/>
</svg>
`);
const avatarBuffer = await sharp(avatarPath)
.resize(avatarSize, avatarSize, { fit: "cover" })
.png()
.toBuffer();
const roundedAvatarBuffer = await sharp(avatarBuffer)
.composite([{ input: maskBuffer, blend: "dest-in" }])
.png()
.toBuffer();
const backgroundBuffer = Buffer.from(`
<svg width="${totalSize}" height="${totalSize}" xmlns="http://www.w3.org/2000/svg">
<circle
cx="${totalSize / 2}"
cy="${totalSize / 2}"
r="${avatarRadius}"
stroke="#ffffff"
stroke-width="${borderSize}"
fill="white"/>
</svg>
`);
return await sharp({
create: {
width: totalSize,
height: totalSize,
channels: 4,
background: "#0000",
}})
.composite([
{ input: backgroundBuffer, top: 0, left: 0 },
{ input: roundedAvatarBuffer, top: borderSize, left: borderSize }
])
.png()
.toBuffer();
}
/**
*
* @param message
* @param outputWidth
* @param outputHeight
* @returns {Promise<void | Buffer<ArrayBufferLike>>}
*/
async getMessageBuffer(message, outputWidth, outputHeight){
return this.getMessageCanvas(message, outputWidth, outputHeight).toBuffer();
//return Buffer.from(messageSvg, "utf-8");
}
/**
*
* @param message {string}
* @param outputWidth
* @param outputHeight
* @returns {string}
*/
/*getMessageSvg(message, outputWidth, outputHeight) {
const safeMessage =
message
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
return `
<svg width="${outputWidth}" height="${outputHeight}" xmlns="http://www.w3.org/2000/svg">
<style>
@font-face {
font-family: 'Fredoka';
src: url("data:font/ttf;base64,${this.fontData}") format('truetype');
}
text {
font-family: 'Fredoka', sans-serif;
fill: #ede6e6;
font-size: 95px;
dominant-baseline: middle;
text-anchor: middle;
}
</style>
<text x="50%" y="50%" dy="0">Welcome ${safeMessage}, to the</text>
<text x="50%" y="50%" dy="1.3em">Spicy Jail ~</text>
</svg>
`;
}*/
/**
*
* @returns {Canvas}
*/
getMessageCanvas(name, outputWidth, outputHeight){
const canvas = createCanvas(outputWidth, outputHeight);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#0000';
ctx.fillRect(0, 0, outputWidth, outputHeight);
ctx.fillStyle = '#ede6e6';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.font = '110px "Fredoka"';
const message = `Welcome ${name}, to the Spicy Jail ~`;
ctx.fillText(message, outputWidth / 2, outputHeight / 2);
return canvas;
}
/*loadFontData(){
const fullPath = path.resolve(this.fontPath);
return fs.readFileSync(fullPath)
.toString("base64")
}*/
}