Electron / NodeJS and application freezing on setInterval / async code - node.js

I'm working on an electron application that performs a screenshot capture every 3 seconds with the electron api, and writes it to a given target path. I've set up a separate BrowserWindow where the capturing code runs in (see code structure below) a setInterval() "loop", but whenever the capture happens, the app freezes for a moment. I think it is the call to source.thumbnail.toPng() or writeScreenshot() method in the file ScreenCapturer.jshtml.js.
I set up this structure as I though this was the way to go, but apparently this is not. WebWorkers won't help me either as I need node modules such as fs, path and desktopCapturer (from electron).
How would one do this type of task without blocking the main thread every time the interval code (as seen in file ScreenCapturer.jshtml.js) runs (because I thought the renderer processes were separate processes?)
My code as reference
main.js (main process)
// all the imports and other
// will only show the import that matters
import ScreenCapturer from './lib/capture/ScreenCapturer';
app.on('ready', () => {
// Where I spawn my main UI
mainWindow = new BrowserWindow({...});
mainWindow.loadURL(...);
// Other startup stuff
// Hee comes the part where I call function to start capturing
initCapture();
});
function initCapture() {
const sc = new ScreenCapturer();
sc.startTakingScreenshots();
}
ScreenCapturer.js (module used by main process)
'use strict';
/* ******************************************************************** */
/* IMPORTS */
import { app, BrowserWindow, ipcMain } from 'electron';
import url from 'url';
import path from 'path';
/* VARIABLES */
let rendererWindow;
/*/********************************************************************///
/*///*/
/* ******************************************************************** */
/* SCREENCAPTURER */
export default class ScreenCapturer {
constructor() {
rendererWindow = new BrowserWindow({
show: true, width: 400, height: 600,
'node-integration': true,
webPreferences: {
webSecurity: false
}
});
rendererWindow.on('close', () => {
rendererWindow = null;
});
}
startTakingScreenshots(interval) {
rendererWindow.webContents.on('did-finish-load', () => {
rendererWindow.openDevTools();
rendererWindow.webContents.send('capture-screenshot', path.join('e:', 'temp'));
});
rendererWindow.loadURL(
url.format({
pathname: path.join(__dirname, 'ScreenCapturer.jshtml.html'),
protocol: 'file:',
slashes: true
})
);
}
}
/*/********************************************************************///
/*///*/
ScreenCapturer.jshtml.js (the thml file loaded in the renderer browser window)
<html>
<body>
<script>require('./ScreenCapturer.jshtml.js')</script>
</body>
</html>
ScreenCapturer.jshtml.js (the js file loaded from the html file in the renderer process)
import { ipcRenderer, desktopCapturer, screen } from 'electron';
import path from 'path';
import fs from 'fs';
import moment from 'moment';
let mainSource;
function getMainSource(mainSource, desktopCapturer, screen, done) {
if(mainSource === undefined) {
const options = {
types: ['screen'],
thumbnailSize: screen.getPrimaryDisplay().workAreaSize
};
desktopCapturer.getSources(options, (err, sources) => {
if (err) return console.log('Cannot capture screen:', err);
const isMainSource = source => source.name === 'Entire screen' || source.name === 'Screen 1';
done(sources.filter(isMainSource)[0]);
});
} else {
done(mainSource);
}
}
function writeScreenshot(png, filePath) {
fs.writeFile(filePath, png, err => {
if (err) { console.log('Cannot write file:', err); }
return;
});
}
ipcRenderer.on('capture-screenshot', (evt, targetPath) => {
setInterval(() => {
getMainSource(mainSource, desktopCapturer, screen, source => {
const png = source.thumbnail.toPng();
const filePath = path.join(targetPath, `${moment().format('yyyyMMdd_HHmmss')}.png`);
writeScreenshot(png, filePath);
});
}, 3000);
});

I walked away from using the API's delivered by electron. I'd recommend using desktop-screenshot package -> https://www.npmjs.com/package/desktop-screenshot. This worked cross platform (linux, mac, win) for me.
Note on windows we need the hazardous package, because otherwise when packaging your electron app with an asar it won't be able to execute the script inside desktop-screenshot. More info on the hazardous package's page.
Below is how my code now roughly works, please don't copy/paste because it might not fit your solution!! However it might give an indication on how you could solve it.
/* ******************************************************************** */
/* MODULE IMPORTS */
import { remote, nativeImage } from 'electron';
import path from 'path';
import os from 'os';
import { exec } from 'child_process';
import moment from 'moment';
import screenshot from 'desktop-screenshot';
/* */
/*/********************************************************************///
/* ******************************************************************** */
/* CLASS */
export default class ScreenshotTaker {
constructor() {
this.name = "ScreenshotTaker";
}
start(cb) {
const fileName = `cap_${moment().format('YYYYMMDD_HHmmss')}.png`;
const destFolder = global.config.app('capture.screenshots');
const outputPath = path.join(destFolder, fileName);
const platform = os.platform();
if(platform === 'win32') {
this.performWindowsCapture(cb, outputPath);
}
if(platform === 'darwin') {
this.performMacOSCapture(cb, outputPath);
}
if(platform === 'linux') {
this.performLinuxCapture(cb, outputPath);
}
}
performLinuxCapture(cb, outputPath) {
// debian
exec(`import -window root "${outputPath}"`, (error, stdout, stderr) => {
if(error) {
cb(error, null, outputPath);
} else {
cb(null, stdout, outputPath);
}
});
}
performMacOSCapture(cb, outputPath) {
this.performWindowsCapture(cb, outputPath);
}
performWindowsCapture(cb, outputPath) {
require('hazardous');
screenshot(outputPath, (err, complete) => {
if(err) {
cb(err, null, outputPath);
} else {
cb(null, complete, outputPath);
}
});
}
}
/*/********************************************************************///

Related

NodeJS contextBridge receiving results from index.js

I've been able to use the preload.js for sending message to API to get things done. I'm able to get responses just fine from iPC, but I'm not able to relay the responses from iPC back to the renderer and I don't understand what I'm missing.
index.js (main)
// Modules to control application life and create native browser window
const { app, BrowserWindow, remote, ipcMain } = require('electron');
const path = require('path');
const { spawn } = require('child_process');
let mainWindow;
const createWindow = () => {
// Create the browser window.
mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
});
// and load the index.html of the app.
mainWindow.loadFile('index.html');
// Open the DevTools.
// mainWindow.webContents.openDevTools()
}
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready',() => {
createWindow();
require('./request.js')(mainWindow);
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});
request.js - this is used in index/main thread above to process API requests, in this case requests to winax which I use with a separate 32 bit nodeJs interpreter because I couldn't get it to build for use with the same version of Node I can use for electron
const { ipcMain, ipcRenderer } = require('electron');
const { spawn } = require('child_process');
module.exports = function(mainWindow){
let winax;
ipcMain.on('winax', (event,arguments) => {
console.log('winax=');
console.log(arguments);
if(winax === undefined) {
console.log('spawning winax');
winax = spawn(
'C:\\Program Files\\nvm\\v14.20.0\\node.exe',
[ 'winax_microamp/index.js' ], {
shell: false,
stdio: ['inherit', 'inherit', 'inherit', 'ipc' ],
windowsHide: true
}
);
}
console.log('sending winax arguments');
/*
arguments = {
method: 'functionToRun',
owner: 'requestingProcess',
params: { required_params ... }
}
*/
winax.send(arguments);
winax.once('message', (message) => {
console.log('winax response=')
console.log(message);
mainWindow.webContents.postMessage('response', message);
});
});
}
preload.js
'use strict';
// preload.js
// All the Node.js APIs are available in the preload process.
// It has the same sandbox as a Chrome extension.
// MAS: This function executes when the DOM is loaded so we should be able to add button interactions here
const { contextBridge, ipcRenderer } = require('electron');
let testAccDb = 'C:\\Users\\PZYVC7\\OneDrive - Ally Financial\\Access\\electron_microamp.accdb';
//const spawn = require('child_process').spawn
contextBridge.exposeInMainWorld(
'request', {
send: (func) => {
console.log('request.send.func=');
console.log(func);
if(func == 'openStagingTest'){
let args = {
method: 'requestStagingDatabase',
owner: 'ui',
params: {
path: testAccDb
}
};
ipcRenderer.send('winax',args);
} else if(func == 'closeStagingTest') {
let args = {
method: 'closeStagingDatabase',
owner: 'ui'
};
ipcRenderer.send('winax',args);
}
},
response: (message) => {
ipcRenderer.on('message', (message) => {
console.log('winax reply=');
console.log(message);
});
}
}
);
window.addEventListener('DOMContentLoaded', () => {
/*const replaceText = (selector, text) => {
const element = document.getElementById(selector);
if (element) element.innerText = text;
}
for (const dependency of ['chrome', 'node', 'electron']) {
replaceText(`${dependency}-version`, process.versions[dependency]);
}*/
});
render.js
'use strict';
onLoad();
function onLoad(){
/*document.querySelector('.one').addEventListener('click',() => {
writeLog()
});*/
/*var testButton = document.querySelector('button[class=test]');
testButton.addEventListener('click', (e) => {
//C:\\Program Files\\nvm\\v14.20.0\\node.exe
window.main.asyncProc( '"C:\\Program Files\\nvm\\v14.20.0\\node.exe" winax_microamp/index.js' );
});*/
var buttons = document.querySelectorAll('button');
buttons.forEach((button) => {
//console.log('button=');
//console.log(button.className);
button.addEventListener('click', (event) => {
window.request.send(button.className);
});
});
}
Ugh... ok, so a few things I figured out eventually, I took hours to figure this out, and I'm not 100% sure if I'm understanding it properly so I appreciate any correction.
I think one issue that threw me off for longer than it should have is that the console.log for the preload.js goes into the developer tools rather than the system console like the main thread areas do.
Another thing throwing me off was that I was writing the implementation inside of preload.js, rather than in render.js. I confirmed it does work in both places.
If I want it in preload.js, I leave the implementation like this, and I confirmed it was firing here based on changing the log message a little
preload.js
response: (message) => {
ipcRenderer.on('response', (message) => {
console.log('expose reply=');
console.log(message);
});
}
}
If I want it in render instead, I need my preload to be more basic
response: (message) => {
ipcRenderer.on('response', message);
}
And then I need render to have this to process it there instead.
window.request.response((event,message) => {
console.log('winax reply=');
console.log(message);
});

Electron Preload vs Electron Main

I am currently using Electron v16. Apparently on this version, we cannot use built-in Node modules in the renderer thread anymore.
The only way to do it is by using electron-preload.js.
I've read this resource:
https://whoisryosuke.com/blog/2022/using-nodejs-apis-in-electron-with-react/
where the author placed the implementation code in electron-main.js by utilizing the ipcMain and the code is invoked through electron-preload.js contextBridge.
My question is:
Is there any difference if put the entire implementation code in electron-preload instead of having the need to invoke an event from here and send it to ipcMain?
Here is the code from the author:
electron-preload.js:
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('electron', {
blenderVersion: async (blenderPath) =>
ipcRenderer.invoke('blender:version', blenderPath),
},
});
electron-main.js
ipcMain.handle('blender:version', async (_, args) => {
console.log('running cli', _, args)
let result
if (args) {
const blenderExecutable = checkMacBlender(args)
// If MacOS, we need to change path to make executable
const checkVersionCommand = `${blenderExecutable} -v`
result = execSync(checkVersionCommand).toString()
}
return result
})
I am thinking if doing something like this instead is acceptable (pros/cons?):
electron-preload.js:
const { contextBridge } = require('electron');
contextBridge.exposeInMainWorld('electron', {
blenderVersion: async (blenderPath) =>
console.log('running cli', _, args)
let result
if (args) {
const blenderExecutable = checkMacBlender(args)
// If MacOS, we need to change path to make executable
const checkVersionCommand = `${blenderExecutable} -v`
result = execSync(checkVersionCommand).toString()
}
return result
},
});
Functionally, there is no difference between the two at all...
That said, separating the calling of your code from its implementation goes a long way in "Separate your Concerns". If you have a lot of calls from render to main (or vice versa), your preload.js script will become huge. Trying to manage a file of that size, especially if you are using typescript in your Electron application can become problematic. The overhead of such declarations will grow and grow.
I take an even more thorough, but in my mind, simplified approach...
I use the preload.js script purely as a place to declare "channel names" and implement the communication between the main process and the render process(es) via the use of pure IPC handles such as:
ipcRenderer.send(channel, ...args)
ipcRenderer.on(channel, listener)
ipcRenderer.invoke(channel, ...args).
Having such a simple script means I only need to manage one preload.js script and I use it in every window I ever create.
This is my preload.js script. To use, just add your "channel names" to whatever whitelisted array you need to. EG: In this instance, I have added the channel name test:channel for use only between the main to render process.
preload.js (main process)
// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [],
// From main to render.
'receive': [
'test:channel' // Our channel name
],
// From render to main and back again.
'sendReceive': []
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
In my main.js script, just IPC with the channel name and any associated data, if any.
main.js (main process)
const electronApp = require('electron').app;
const electronBrowserWindow = require('electron').BrowserWindow;
const nodePath = require("path");
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.webContents.send('test:channel', 'Hello from the main thread') })
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
Finally, in your render just listen for the channel name and implement functionality when called.
index.html (render process)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Electron Test</title>
</head>
<body>
<div id="test"></div> // Outputs "Hello from the main thread"
</body>
<script>
window.ipcRender.receive('test:channel', (text) => {
document.getElementById('test').innerText = text;
})
</script>
</html>
To use this preload.js scipt in the main and render processes, the following implementation would apply.
/**
* Render --> Main
* ---------------
* Render: window.ipcRender.send('channel', data); // Data is optional.
* Main: electronIpcMain.on('channel', (event, data) => { methodName(data); })
*
* Main --> Render
* ---------------
* Main: windowName.webContents.send('channel', data); // Data is optional.
* Render: window.ipcRender.receive('channel', (data) => { methodName(data); });
*
* Render --> Main (Value) --> Render
* ----------------------------------
* Render: window.ipcRender.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', (event, data) => { return someMethod(data); });
*
* Render --> Main (Promise) --> Render
* ------------------------------------
* Render: window.ipcRender.invoke('channel', data).then((result) => { methodName(result); });
* Main: electronIpcMain.handle('channel', async (event, data) => {
* return await promiseName(data)
* .then(() => { return result; })
* });
*/
Remember, you can't return functions, promises or other objects that can't be serialised with the structured clone algorithm. See Object serialization for more information.

How to get the path to the directory of the opened file?

I'm Kenyon Bowers.
I have some code that opens a open file dialog. It opens .DSCProj (which are specific to my project), and I am going to run some terminal commands in the directory that the opened file is in.
I have no idea how to do that.
preload.ts:
import { ipcRenderer, contextBridge } from "electron";
import { dialog } from '#electron/remote'
contextBridge.exposeInMainWorld("api", {
showOpenFileDialog: () => dialog.showOpenDialogSync({
properties: ["openFile"],
filters: [
{
name: "DSC Projects",
extensions: ["DSCProj"],
},
],
})
});
NewProject.ts:
declare var api: any;
function OpenProject(): void {
const file = api.showOpenFileDialog();
console.log("Done")
if(file != null){
localStorage.setItem('DirPath', file);
location.href='./views/projectOpen.html'
}
}
(() => {
document.querySelector('#btn-open-project')?.addEventListener('click', () => {
OpenProject();
}),
document.querySelector('#btn-new-project')?.addEventListener('click', () => {
location.href='./views/projectNew.html'
})
})()
As you can see on line 7, I'm setting local storage to the file's path. But I need to set it to the path of the directory that the file is in.
Whilst use of #electron/remote is great and all, you may be better served by implementing certain Electron modules within the main thread instead of the render thread(s). This is primarily for security but as a strong second reason, it keeps your code separated. IE: Separation of concerns.
Unlike vanilla Javascript, node.js has a simple function path.parse(path).dir to easily remove the file name (and extension) from the file path without needing to worry about which OS (IE: Directory separator) you are using. This would also be implemented within your main thread. Implementing something like this within your render thread would take a lot more work with vanilla Javascript to be OS proof.
Lastly, in the code below I will use a preload.js script that only deals with the movement of messages and their data between the main thread and render thread(s). I do not believe that the concrete implementation of functions in your preload.js script(s) is the right approach (though others may argue).
Note: I am not using typescript in the below code, but you should get the general idea.
Let's use the channel name getPath within the invoke method.
preload.js (main thread)
// Import the necessary Electron components.
const contextBridge = require('electron').contextBridge;
const ipcRenderer = require('electron').ipcRenderer;
// White-listed channels.
const ipc = {
'render': {
// From render to main.
'send': [],
// From main to render.
'receive': [],
// From render to main and back again.
'sendReceive': [
'getPath'
]
}
};
// Exposed protected methods in the render process.
contextBridge.exposeInMainWorld(
// Allowed 'ipcRenderer' methods.
'ipcRender', {
// From render to main.
send: (channel, args) => {
let validChannels = ipc.render.send;
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, args);
}
},
// From main to render.
receive: (channel, listener) => {
let validChannels = ipc.render.receive;
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`.
ipcRenderer.on(channel, (event, ...args) => listener(...args));
}
},
// From render to main and back again.
invoke: (channel, args) => {
let validChannels = ipc.render.sendReceive;
if (validChannels.includes(channel)) {
return ipcRenderer.invoke(channel, args);
}
}
}
);
Now, in the main thread, let's listen for a call on the getPath channel. When called, open up the dialog and upon the return of a path, process it with Node's path.parse(path).dir function to remove the file name (and extension). Lastly, return the modified path.
main.js (main thread)
const electronBrowserWindow = require('electron').BrowserWindow;
const electronDialog = require('electron').dialog;
const electronIpcMain = require('electron').ipcMain;
const nodePath = require("path");
let window;
function createWindow() {
const window = new electronBrowserWindow({
x: 0,
y: 0,
width: 800,
height: 600,
show: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: nodePath.join(__dirname, 'preload.js')
}
});
window.loadFile('index.html')
.then(() => { window.show(); });
return window;
}
electronApp.on('ready', () => {
window = createWindow();
});
electronApp.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
electronApp.quit();
}
});
electronApp.on('activate', () => {
if (electronBrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// -----
// Let's listen for a call on the 'getPath' channel
electronIpcMain.handle('getPath', async () => {
// Dialog options.
const options = {
properties: ["openFile"],
filters: [
{
name: "DSC Projects",
extensions: ["DSCProj"],
}
]
}
// When available, return the modified path back to the render thread via IPC
return await openDialog(window, options)
.then((result) => {
// User cancelled the dialog
if (result.canceled === true) { return; }
// Modify and return the path
let path = result.filePaths[0];
let modifiedPath = nodePath.parse(path).dir; // Here's the magic.
console.log(modifiedPath); // Testing
return modifiedPath;
})
})
// Create an open dialog
function openDialog(parentWindow, options) {
return electronDialog.showOpenDialog(parentWindow, options)
.then((result) => { if (result) { return result; } })
.catch((error) => { console.error('Show open dialog error: ' + error); });
}
Here you will get the general idea about how to use the returned result.
index.html (render thread)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Open Project Test</title>
</head>
<body>
<input type="button" id="btn-open-project" value="Open Project">
</body>
<script>
document.getElementById('btn-open-project').addEventListener('click', () => {
openProject();
});
function openProject() {
window.ipcRender.invoke('getPath')
.then((path) => {
// As we are using "invoke" a response will be returned, even if undefined.
if (path === undefined) { return; } // When user cancels dialog.
console.log(path); // Testing.
// window.localStorage.setItem('DirPath', path);
// location.href='./views/projectOpen.html';
});
}
</script>
</html>

How to gracefully shutdown bullmq when running inside an express server?

I have an express app which uses bullmq queues, schedulers and workers. Even after pressing Ctrl + C I can still see the node process running inside my Activity manager but my server on the terminal shuts down. I know this because the bullmq task starts outputting console.log statements even after the server is down to the terminal.
This is what my server.js file looks like
// eslint-disable-next-line import/first
import http from 'http';
import { app } from './app';
import { sessionParser } from './session';
import { websocketServer } from './ws';
import 'jobs/repeatable';
const server = http.createServer(app);
server.on('upgrade', (request, socket, head) => {
sessionParser(request, {}, () => {
websocketServer.handleUpgrade(request, socket, head, (ws) => {
websocketServer.emit('connection', ws, request);
});
});
});
server.on('listening', () => {
websocketServer.emit('listening');
});
server.on('close', () => {
websocketServer.emit('close');
});
// https://stackoverflow.com/questions/18692536/node-js-server-close-event-doesnt-appear-to-fire
process.on('SIGINT', () => {
server.close();
});
export { server };
Notice that I have a SIGINT handler defined above. Is this the reason my jobs are not exiting? Do I have to manually close every queue, worker and scheduler inside my SIGINT? My jobs/repeatable.js file looks as shown below
const { scheduleJobs } = require('jobs');
if (process.env.ENABLE_JOB_QUEUE === 'true') {
scheduleJobs();
}
Here is my jobs.js file
import { scheduleDeleteExpiredTokensJob } from './delete-expired-tokens';
import { scheduleDeleteNullVotesJob } from './delete-null-votes';
export async function scheduleJobs() {
await scheduleDeleteExpiredTokensJob();
await scheduleDeleteNullVotesJob();
}
Here is my delete-expired-tokens.js file, other one is quite similar
import { processor as deleteExpiredTokensProcessor } from './processor';
import { queue as deleteExpiredTokensQueue } from './queue';
import { scheduler as deleteExpiredTokensScheduler } from './scheduler';
import { worker as deleteExpiredTokensWorker } from './worker';
export async function scheduleDeleteExpiredTokensJob() {
const jobId = process.env.QUEUE_DELETE_EXPIRED_TOKENS_JOB_ID;
const jobName = process.env.QUEUE_DELETE_EXPIRED_TOKENS_JOB_NAME;
await deleteExpiredTokensQueue.add(jobName, null, {
repeat: {
cron: process.env.QUEUE_DELETE_EXPIRED_TOKENS_FREQUENCY,
jobId,
},
});
}
export {
deleteExpiredTokensProcessor,
deleteExpiredTokensQueue,
deleteExpiredTokensScheduler,
deleteExpiredTokensWorker,
};
How do I shutdown bullmq task queues gracefully?
You have to call the close() method on the workers:
server.on('close', async () => {
websocketServer.emit('close');
// Close the workers
await worker.close()
});
Docs

Jest error "SyntaxError: Need to install with `app.use` function" when using vue-i18n plugin for Vue3

I am using vue-i18n plugin for my Vue3(typescript) application. Below is my setup function in component code
Home.vue
import {useI18n} from 'vue-i18n'
setup() {
const {t} = useI18n()
return {
t
}
}
Main.ts
import { createI18n } from 'vue-i18n'
import en from './assets/translations/english.json'
import dutch from './assets/translations/dutch.json'
// internationalization configurations
const i18n = createI18n({
messages: {
en: en,
dutch: dutch
},
fallbackLocale: 'en',
locale: 'en'
})
// Create app
const app = createApp(App)
app.use(store)
app.use(router)
app.use(i18n)
app.mount('#app')
Code works and compiles fine. But jest test cases fails for the component when it's mounting
Spec file
import { mount, VueWrapper } from '#vue/test-utils'
import Home from '#/views/Home.vue'
import Threat from '#/components/Threat.vue'
// Test case for Threats Component
let wrapper: VueWrapper<any>
beforeEach(() => {
wrapper = mount(Home)
// eslint-disable-next-line #typescript-eslint/no-empty-function
jest.spyOn(console, 'warn').mockImplementation(() => { });
});
describe('Home.vue', () => {
//child component 'Home' existance check
it("Check Home component exists in Threats", () => {
expect(wrapper.findComponent(Home).exists()).toBe(true)
})
// Threat level list existance check
it("Check all 5 threat levels are listed", () => {
expect(wrapper.findAll('.threat-level .level-wrapper label')).toHaveLength(5)
})
})
Below is the error
Please help me to resolve this.
The vue-18n plugin should be installed on the wrapper during mount with the global.plugins option:
import { mount } from '#vue/test-utils'
import { createI18n } from 'vue-i18n'
import Home from '#/components/Home.vue'
describe('Home.vue', () => {
it('i18n', () => {
const i18n = createI18n({
// vue-i18n options here ...
})
const wrapper = mount(Home, {
global: {
plugins: [i18n]
}
})
expect(wrapper.vm.t).toBeTruthy()
})
})
GitHub demo
You can also define the plugin globally in the setup/init file:
import { config } from '#vue/test-utils'
import { createI18n } from 'vue-i18n'
const i18n = createI18n({
// vue-i18n options here ...
})
config.global.plugins = [i18n]
config.global.mocks.$t = (key) => key

Resources