Electron-preload for node methods - node.js

I am working on converting my Electron project to use Electron v16 which uses contextBridge for the Node built-in modules.
I currently have this, which uses the net module:
import { Socket } from 'net'
async function portConnection (portConfig) {
const client = await new Socket()
client.setKeepAlive(true, 60000)
return new Promise((resolve, reject) => {
client.connect(portConfig.port, portConfig.host)
client.on('connect', () => {
return resolve(client)
})
client.on('timeout', () => {
return reject(new Error('Connection timeout'))
})
client.on('error', (error) => {
return reject(error)
})
})
}
This gives me an error:
TypeError: client.setKeepAlive is not a function
So I added this to the webpack config:
// I know this is not advised, but for simplicity I tried to overwrite the
// 'net' module with one that I preloaded from electron-preload
Object.assign(cfg.externals, {
net: 'window.netPreload'
})
Then I also moved the setKeepAlive function to electron-preload.js:
import { Socket } from 'net'
let client = null
contextBridge.exposeInMainWorld('netPreload', require('net'))
contextBridge.exposeInMainWorld('netApi', {
async createClient () {
client = await new Socket()
return client
},
async keepAlive () {
if (!client) console.error('No client defined')
client.setKeepAlive(true, 60000)
},
async connect (port, host) {
if (!client) console.error('No client defined')
client.connect(port, host)
}
})
Then I call it like this:
async function portConnection (portConfig) {
const client = await window.netApi.createClient()
await window.netApi.setKeepAlive()
return new Promise((resolve, reject) => {
client.connect(portConfig.port, portConfig.host)
client.on('connect', () => {
return resolve(client)
})
client.on('timeout', () => {
return reject(new Error('Connection timeout'))
})
client.on('error', (error) => {
return reject(error)
})
})
}
Now I am getting an error:
TypeError: client.on is not a function
I dont really want to add a contextBridge method for each method in 'net' module. Is there any way to do this?
Also, in this case, how can I 'overwrite' with a polyfill the <net.Server> returned by the command new Socket()?

Related

SyntaxError: Unexpected reserved word "await", node.js is correct version

I am trying to run a function asynchronously 20 times.
I have the function definition:
import axios from 'axios';
async function updateUser (url, user) {
return new Promise((resolve, reject) => {
axios.put(url, JSON.stringify(user))
.then(response => {
//Folio returned code 204
console.log('The user has been updated');
resolve(response.data);
}).catch((err) => {
//Folio returned error
console.error(`Error Text: ${err.response.data}`);
console.error(`Error Code: ${err}`);
});
});
};
export {updateUser};
Then I am trying to loop through users in an external JSON, and update them to my system 20 at a time:
import {updateUser} from './updateUser.js';
import usersjson from './jsons/users.json';
let promises=[];
if (usersjson.users.length) {
try {
for (const user of usersjson.users) {
update = new promise ((resolve,reject) => {
updateUser(url, user)
.then((list) => {resolve(list)})
.catch((error)=>{reject(error)})
});
promises.push(update);
if (promises.length = 20) {
await Promise.all(promises)
.then((responses)=> {
console.log(responses);
promises=[];
}).catch((err)=> {
console.log(err);
});
}
}
} catch(err)=> {
console.log(err);
}
}
However, I am getting an error message in the console:
await Promise.all(promises)
^^^^^
SyntaxError: Unexpected reserved word
My node version is 12.19.0 (based on previous questions, it seems it has to be at least 10, so that shouldn't be the issue).
When I run the function without await, they work correctly (i.e. the users are updated on my system).
It also seems that the for loop is asynchronous by itself, but I'm afraid that if there will be too many calls simultaneously my system will block the API, so I want to make 20-50 calls at a time, tops.
In order to use await you need to declare the function in which you are putting the await as async
For example:
const functionReturningPromise = (input) => {
return new Promise((resolve, reject) => {
if (!input) {
return reject();
}
return resolve();
});
}
const functionAwaitingPromise = async () => {
for (let i = 0; i < 20; i +=1) {
try {
await functionReturningPromise(i);
console.log(i);
} catch (error) {
console.log(error);
}
}
}

Jest - how to mock a Class used by a module?

I have a class :
class RequestTimeout {
constructor(timeoutMilliseconds) {
this.timeoutMilliseconds = timeoutMilliseconds;
this.timeoutID = undefined;
}
start() {
return new Promise((resolve, reject) => {
this.timeoutID = setTimeout(() => reject(new Error(`Request attempt exceeded timeout of ${this.timeoutMilliseconds}`)), this.timeoutMilliseconds);
});
}
clear() {
if (this.timeoutID) clearTimeout(this.timeoutID);
}
}
module.exports = RequestTimeout;
This class is used in a module:
const RequestTimeout = require('./request-timeout');
function Request() {
...
async function withTimeout(request, ms) {
const timeout = new RequestTimeout(ms);
return Promise.race([
request(),
timeout.start(),
])
.then(
response => {
timeout.clear();
return response;
},
err => {
timeout.clear();
throw err;
}
);
}
...
}
How do i mock RequestTimeout in a test using Request? For example:
it('should clear the timeout following a successful response', async () => {
nock('http://example.com')
.get('/')
.reply(200, { example: true });
const response = await request.get({ ...baseOptions });
expect(response.example).toEqual(true);
});
// MOCK
let mockGetTimeOutId = jest.fn();
jest.mock('../request-timeout', () => {
return jest.fn().mockImplementation((ms) => {
let timeoutId = undefined;
return {
start: () => new Promise((resolve, reject) => {
timeoutId = setTimeout(() => reject(), ms);
}),
clear: () => mockGetTimeOutId(timeoutId),
}
})
});
// TEST
it('should clear the timeout following a successful response', async () => {
nock('http://example.com')
.get('/')
.reply(200, { example: true });
expect(mockGetTimeOutId).toHaveBeenCalledTimes(0);
const response = await request.get({ ...baseOptions });
expect(mockGetTimeOutId).toHaveBeenCalledTimes(1);
expect(response.example).toEqual(true);
});

Request promises with actions-on-google node v2

I have a similar problem like Use Async Functions in DialogFlow WebHook but that solution, changing request-promises for request-promises-native, didn't work for me, the difference it's that I'm using actions-on-google lib with ActionsSDK instead DialogFlow one, here is my code:
function call() {
var options = {
url: "https://google.es"
};
return request(options)
.then((res) => {
console.log("Success", res);
Promise.resolve();
})
.catch((err) => {
console.log("Error", err);
Promise.resolve();
});
}
const handleAction = (conv) => {
call()
.then(() => {
console.log("Going to ASK");
conv.ask('Hi, how is it going?');
return Promise.resolve();
})
.catch(error => {
console.log("Ask ERROR");
conv.ask('Hi, how is it going?');
return Promise.resolve();
});
}
app.intent('actions.intent.MAIN', (conv) => {
handleAction(conv);
});
If I change the call function for this one:
function call() {
let prom = new Promise((resolve,reject) =>{
resolve();
});
return prom;
}
It works like a charm. I don't understand where I'm wrong, I'm returning promises until the intent. Any idea?
Thanks!
you can resolve this problem using async/await. It will look like this. It might help you.
(async () => {
async function call() {
var options = {
url: "https://google.es"
};
return await new Promise((resolve, reject) => {
request(options)
.then((res) => {
resolve(res);
})
.catch((err) => {
console.log("Error", err);
reject(err)
});
}
}
const handleAction = await call(); //you will get result on handle Action varaible
})();

What's wrong with this use of async await?

I am trying to download tracks via the soundcloud API, and then launch a callback once an indeterminant amount of tracks is downloaded. When I run the below code, I see "All done" being console logged before anything else, even though I intend for it to be the last thing... What am I doing wrong?
// Deps
import fs from 'fs'
import SC from 'node-soundcloud'
import request from 'request'
// Write mp3 function
function writeMP3(track) {
return new Promise((resolve, reject) => {
console.log('Starting download: ', track.title)
request.get(track.download_url)
.on('error', err => {
// reject('Download error: ', err)
})
.on('finish', () => {
() => resolve('Download complete')
})
.pipe(fs.createWriteStream(`./data/temp/${track.title}_${track.user.username}.mp3`))
})
}
async function asyncTrackFetch(track) {
return await writeMP3(track)
}
// Array of promises to callback upon
const trackActions = []
SC.init({
id: 'MY_ID',
secret: 'MY_SECRET'
})
SC.get('/tracks', (err, tracks) => {
if (err) {
throw new Error(err)
} else {
console.log('Tracks fetched: ', tracks.length)
tracks.map(track => {
if (track.downloadable) {
console.log('downloadable')
trackActions.push(asyncTrackFetch(track))
}
})
}
})
// Perform requests async
Promise.all(trackActions).then(() => {
console.log('All done')
console.log(fs.readdirSync('./data/temp'))
})
Promise.all(trackActions) waits on whatever promises are in trackActions, but trackActions is empty at the time you make the call. You're only adding promises to the array after your SC.get callback gets called.
Try putting your Promise.all... block inside the SC.get callback like this:
SC.get('/tracks', (err, tracks) => {
if (err) {
throw new Error(err)
} else {
console.log('Tracks fetched: ', tracks.length)
tracks.map(track => {
if (track.downloadable) {
console.log('downloadable')
trackActions.push(asyncTrackFetch(track))
}
})
Promise.all(trackActions).then(() => {
console.log('All done')
console.log(fs.readdirSync('./data/temp'))
})
}
})
It's worth mentioning as well that your line throw new Error(err) will crash the program since there's nowhere for that error to be caught.
As Antonio Val mentioned, there are better ways to do this. If you promisify the node-soundcloud library then the last part of your code could look like this:
SC.get('/tracks').then(tracks => {
// No need for trackedActions array.
return Promise.all(tracks.filter(track => track.downloadable)
.map(track => asyncTrackFetch(track)))
}).then(fetchedTracks => {
console.log('All done fetching tracks', fetchedTracks)
}).catch(err => {
// Handle error.
})
Or inside an async function,
try {
const tracks = await SC.get('/tracks')
const fetchPromises = tracks
.filter(track => track.downloadable)
.map(track => asyncTrackFetch(track))
const fetchedTracks = await Promise.all(fetchPromises)
console('All done fetching tracks.', fetchedTracks)
} catch (err) {
// Handle error
}
I think the easiest way would be to move Promise.all after tracks.map loop finished.
A more elegant solution would be to promisify SC.get as well and use async await along all your code.
UPDATE:
Couldn't test it so not sure if it works, but it would be something like this:
import fs from 'fs'
import SC from 'node-soundcloud'
import request from 'request'
function writeMP3(track) {
return new Promise((resolve, reject) => {
console.log('Starting download: ', track.title)
request.get(track.download_url)
.on('error', err => {
// reject('Download error: ', err)
})
.on('finish', () => {
() => resolve('Download complete')
})
.pipe(fs.createWriteStream(`./data/temp/${track.title}_${track.user.username}.mp3`))
})
}
function getTracks() {
return new Promise((resolve, reject) => {
SC.get('/tracks', (err, tracks) => {
if (err) {
return reject(err)
}
console.log('Tracks fetched: ', tracks.length)
resolve(tracks)
})
})
}
SC.init({
id: 'MY_ID',
secret: 'MY_SECRET'
})
With async await:
async function start() {
const tracks = await getTracks();
for (let track of tracks) {
await writeMP3(track)
}
}
start()
.then(() => {
console.log('All done')
console.log(fs.readdirSync('./data/temp'))
})
.catch((err) => {
// insert error handler here
})
If you just want to use Promises:
getTracks
.then((tracks) => {
const promiseArray = tracks.map((track) => {
return writeMP3(track)
})
return Promise.all(promiseArray)
})
.then(() => {
console.log('All done')
console.log(fs.readdirSync('./data/temp'))
})
.catch((err) => {
// insert error handler here
})

Sinon to unit test 'catch' statement

I've got a simple function such as;
module.exports = {
fetchUser:function(myUserId) {
return new Promise((resolve, reject) => {
this.getUser(myUserId)
.then(user => {
// do logic // then return user
return user;
})
.then(resolve)
.catch(err => {
// whoops there has been an error
let error = { error: 'My Error' };
reject(error);
});
});
}
};
I want to unit test both the resolve and reject result.
A simple chai test would be;
var expect = require('chai').expect;
var user = require('./user');
describe('User module', function() {
it('test fetchUser', function() {
let _user = user.fetchUser('abc123');
return _user
.then(user => {
expect(data).to.be.an('object');
});
});
Using sinon or another library, how can I for the fetchUser function to throw that reject error?
With Mocha, Chai and Sinon it can be implemented with stubbed method getUser.
const User = require("./fetchUserModule");
describe('User module', () => {
beforeEach(() => User.getUser = sinon.stub());
afterEach(() => User.getUser.reset());
it('returns user if `getUser` returns data', () => {
const user = {name: 'John'};
User.getUser.withArgs("abc123").returns(Promise.resolve(user));
return User.fetchUser("abc123").then(result => {
expect(result).to.equal(user)
}).catch(error => {
expect(error).to.be.undefined;
})
});
it('throws error if `getUser` is rejected', () => {
User.getUser.withArgs("abc123").returns(Promise.reject());
return User.fetchUser("abc123").then(result => {
expect(result).to.be.undefined;
}).catch(err => {
expect(err).to.eql({error: 'My Error'})
})
});
});
Start with anything in your "logic" that can throw an error.
If not you would need to stub this.getUser to reject or throw an error instead of returning data. sinon-as-promised patches sinon.stub to include the .resolves and .rejects promise helpers.
const sinon = require('sinon')
require('sinon-as-promised')
Setup the stub for the failure tests.
before(function(){
sinon.stub(user, 'getUser').rejects(new Error('whatever'))
})
after(function(){
user.getUser.restore()
})
Then either catch the .fetchUser error or use chai-as-promised for some sugar.
it('test fetchUser', function() {
return user.fetchUser('abc123')
.then(()=> expect.fail('fetchUser should be rejected'))
.catch(err => {
expect(err.message).to.eql('whatever')
})
})
it('test fetchUser', function() {
return expect(user.fetchUser('abc123')).to.be.rejectedWith(Error)
})
or async if you live in the new world
it('test fetchUser', async function() {
try {
await user.fetchUser('abc123')
expect.fail('fetchUser should be rejected'))
} catch(err) {
expect(err.message).to.eql('whatever')
}
})
As a side note, you don't need to wrap something that already returns a promise in new Promise and be careful about losing error information when chaining multiple .catch handlers.
fetchUser: function (myUserId) {
return this.getUser(myUserId)
.then(user => {
//logic
return user
})
.catch(err => {
let error = new Error('My Error')
error.original = err
reject(error)
});
}

Resources