Description
I am writing a Discord Bot in NodeJS and am currently experiencing a very odd issue.
What I am wanting to do is to get the result of health via method getHP(),
afterwards updating the health property with the setHP() method.
This works for one class, but not for another. So basically, the code is practically the same, but for the other class it does not update the property.
I am calling both the classes their setHP() methods in their constructors.
Code:
// Player.js - This works and displays: { current: 98, max: 98 }
class Player {
constructor(member, msg) {
this.member = member
this.msg = msg
this.setHP()
}
health = {}
setHP() {
this.getHP.then(hp => {
this.health = { current: hp.current, max: hp.current }
})
}
get getHP() {
return new Promise(async (resolve) => {
const stats = await this.stats
resolve(stats.find(stat => stat.id === 'health'))
})
}
get stats() {
return new Promise(async (resolve) => {
const result = await DB.query(`select stats from members where member_id = ${this.member.id} `)
resolve(JSON.parse(result[0][0].stats))
})
}
get difficulty() {
return new Promise(async (resolve) => {
const result = await DB.query(`select difficulty from members where member_id = ${this.member.id} `)
resolve(result[0][0].difficulty)
})
}
}
// Enemy.js - Doesn't work and displays: {}
class Enemy {
constructor(player) {
this.player = player
this.setHP()
}
hp = {}
setHP() {
this.getHP.then(int => {
this.hp = { current: int, max: int }
})
}
get getHP() {
return new Promise(async (resolve) => {
const difficulty = await this.player.difficulty
const int = Math.floor(this.player.health.current * (difficulty * (Math.random() * 0.10 + 0.95)))
resolve(int)
})
}
// minion_fight.js - Where the classes are used
const Enemy = require("Enemy.js")
const Player = require("Player.js")
module.exports.execute = async (msg) => {
const player = new Player(msg.member, msg)
const enemy = new Enemy(player)
// ...
}
The main issue is that the player instance has a pending promise that will eventually resolve and set the player's health property. But before that happens, the enemy instance is created, and it accesses the above mentioned health property from the given player before it has been set. So this.player.health.current cannot be evaluated.
It is better to:
Avoid launching asynchronous tasks in a constructor. Instead create methods that do this.
Avoid creating promises with new Promise, when there is already a promise to await. This is an anti-pattern.
Don't use getters/setters for asynchronous tasks. Just make them asynchronous methods -- it will make the code easier to understand.
Please terminate your statements with a semi-colon. You don't really want to make the interpretation of your code dependent on the automatic semi-colon insertion algorithm.
Here is the suggested correction -- but I didn't test it, so I hope you'll at least get the gist of the proposed changes:
// Player.js
class Player {
constructor(member, msg) {
this.member = member;
this.msg = msg;
}
health = {}
async setHP() {
const hp = await this.getHP();
this.health = { current: hp.current, max: hp.current };
return this.health;
}
async getHP() {
const stats = await this.stats();
return stats.find(stat => stat.id === 'health');
}
async stats() {
const result = await DB.query(`select stats from members where member_id = ${this.member.id} `);
return JSON.parse(result[0][0].stats);
}
async difficulty() {
const result = await DB.query(`select difficulty from members where member_id = ${this.member.id} `);
return result[0][0].difficulty;
}
}
// Enemy.js
class Enemy {
constructor(player) {
this.player = player;
}
hp = {}
async setHP() {
const current = await this.getHP();
this.hp = { current, max: int };
return this.hp;
}
async getHP() {
const playerHealth = await this.player.getHP(); // To be sure the promise is resolved!
const difficulty = await this.player.difficulty();
return Math.floor(playerHealth.current * (difficulty * (Math.random() * 0.10 + 0.95)));
}
}
// minion_fight.js
const Enemy = require("Enemy.js")
const Player = require("Player.js")
module.exports.execute = async (msg) => {
const player = new Player(msg.member, msg);
const enemy = new Enemy(player);
await enemy.setHP();
// ...
}
Related
context: Two javascript classes in separate files, each integrating a different external service and being called in a express.js router.
See "problematic code" below:
route
routes.post('/aws', upload.single('file'), async (req, res) => {
const transcribeParams = JSON.parse(req.body.options)
const bucket = 'bucket-name'
const data = await ( await ( await awsTranscribe.Upload(req.file, bucket)).CreateJob(transcribeParams)).GetJob()
res.send(data)
})
S3 class
class AmazonS3 {
constructor() {
this.Upload = this.Upload
}
async Upload(file, bucket) {
const uploadParams = {
Bucket: bucket,
Body: fs.createReadStream(file.path),
Key: file.filename,
}
this.data = await s3.upload(uploadParams).promise()
return this
}
}
Transcribe class
class Transcribe extends AwsS3 {
constructor() {
super()
this.CreateJob = this.CreateJob
this.GetJob = this.GetJob
}
async CreateJob(params) {
if(this.data?.Location) {
params.Media = { ...params.Media, MediaFileUri: this.data.Location }
}
this.data = await transcribeService.startTranscriptionJob(params).promise()
return this
}
async GetJob(jobName) {
if(this.data?.TranscriptionJob?.TranscriptionJobName) {
jobName = this.data.TranscriptionJob.TranscriptionJobName
}
this.data = await transcribeService.getTranscriptionJob({TranscriptionJobName: jobName}).promise()
return this
}
}
problem: the problem is with the chained awaits in the router file:
await ( await ( await awsTranscribe.Upload...
Yes, it does work, but it would be horrible for another person to maintain this code in the future.
How can i make so it would be just
awsTranscribe.Upload(req.file, bucket).CreateJob(transcribeParams).GetJob() without the .then?
The problem is with the chained awaits in the router file: await ( await ( await awsTranscribe.Upload...
No, that's fine. In particular it would be trivial to refactor it to separate lines:
routes.post('/aws', upload.single('file'), async (req, res) => {
const transcribeParams = JSON.parse(req.body.options)
const bucket = 'bucket-name'
const a = await awsTranscribe.Upload(req.file, bucket);
const b = await b.CreateJob(transcribeParams);
const c = await b.GetJob();
res.send(c);
});
Your actual problem is that a, b, and c all refer to the same object awsTranscribe. Your code would also "work" if it was written
routes.post('/aws', upload.single('file'), async (req, res) => {
const transcribeParams = JSON.parse(req.body.options)
const bucket = 'bucket-name'
await awsTranscribe.Upload(req.file, bucket);
await awsTranscribe.CreateJob(transcribeParams);
await awsTranscribe.GetJob();
res.send(awsTranscribe);
});
The horrible thing is that you are passing your data between these methods through the mutable awsTranscribe.data property - even storing different kinds of data in it at different times! One could change the order of method calls and it would completely break in non-obvious and hard-to-debug ways.
Also it seems that multiple requests share the same awsTranscribe instance. This will not work with concurrent requests. Anything is possible from just "not working" to responding with the job data from a different user (request)! You absolutely need to fix that, then look at ugly syntax later.
What you really should do is get rid of the classes. There's no reason to use stateful objects here, this is plain procedural code. Write simple functions, taking parameters and returning values:
export async function uploadFile(file, bucket) {
const uploadParams = {
Bucket: bucket,
Body: fs.createReadStream(file.path),
Key: file.filename,
};
const data = s3.upload(uploadParams).promise();
return data.Location;
}
export async function createTranscriptionJob(location, params) {
params = {
...params,
Media: {
...params.Media,
MediaFileUri: location,
},
};
const data = await transcribeService.startTranscriptionJob(params).promise();
return data.TranscriptionJob;
}
async function getTranscriptionJob(job) {
const jobName = job.TranscriptionJobName;
return transcribeService.getTranscriptionJob({TranscriptionJobName: jobName}).promise();
}
Then you can import and call them as
routes.post('/aws', upload.single('file'), async (req, res) => {
const transcribeParams = JSON.parse(req.body.options)
const bucket = 'bucket-name'
const location = await uploadFile(req.file, bucket);
const job = await createTranscriptionJob(location, transcribeParams);
const data = await getTranscriptionJob(job);
res.send(c);
});
I got interested in whether it was possible to take an object with several async methods and somehow make them automatically chainable. Well, you can:
function chain(obj, methodsArray) {
if (!methodsArray || !methodsArray.length) {
throw new Error("methodsArray argument must be array of chainable method names");
}
const methods = new Set(methodsArray);
let lastPromise = Promise.resolve();
const proxy = new Proxy(obj, {
get(target, prop, receiver) {
if (prop === "_promise") {
return function() {
return lastPromise;
}
}
const val = Reflect.get(target, prop, receiver);
if (typeof val !== "function" || !methods.has(prop)) {
// no chaining if it's not a function
// or it's not listed as a chainable method
return val;
} else {
// return a stub function
return function(...args) {
// chain a function call
lastPromise = lastPromise.then(() => {
return val.apply(obj, args);
//return Reflect.apply(val, obj, ...args);
});
return proxy;
}
}
}
});
return proxy;
}
function delay(t) {
return new Promise(resolve => {
setTimeout(resolve, t);
});
}
function log(...args) {
if (!log.start) {
log.start = Date.now();
}
const delta = Date.now() - log.start;
const deltaPad = (delta + "").padStart(6, "0");
console.log(`${deltaPad}: `, ...args)
}
class Transcribe {
constructor() {
this.greeting = "Hello";
}
async createJob(params) {
log(`createJob: ${this.greeting}`);
return delay(200);
}
async getJob(jobName) {
log(`getJob: ${this.greeting}`);
return delay(100);
}
}
const t = new Transcribe();
const obj = chain(t, ["getJob", "createJob"]);
log("begin");
obj.createJob().getJob()._promise().then(() => {
log("end");
});
There's a placeholder for your Transcribe class that has two asynchronous methods that return a promise.
Then, there's a chain() function that returns a proxy to an object that makes a set of passed in method names be chainable which allows you to then do something like this:
const t = new Transcribe();
// make chainable proxy
const obj = chain(t, ["getJob", "createJob"]);
obj.createJob().getJob()
or
await obj.createJob().getJob()._promise()
I wouldn't necessarily say this is production-ready code, but it is an interesting feasibility demonstration and (for me) a chance to learn more about a Javascript proxy object.
Here's a different approach that (instead of the proxy object) adds method stubs to a promise to make things chainable:
function chain(orig, methodsArray) {
let masterP = Promise.resolve();
function addMethods(dest) {
for (const m of methodsArray) {
dest[m] = function(...args) {
// chain onto master promise to force sequencing
masterP = masterP.then(result => {
return orig[m].apply(orig, ...args);
});
// add methods to the latest promise befor returning it
addMethods(masterP);
return masterP;
}
}
}
// add method to our returned promise
addMethods(masterP);
return masterP;
}
function delay(t) {
return new Promise(resolve => {
setTimeout(resolve, t);
});
}
function log(...args) {
if (!log.start) {
log.start = Date.now();
}
const delta = Date.now() - log.start;
const deltaPad = (delta + "").padStart(6, "0");
console.log(`${deltaPad}: `, ...args)
}
class Transcribe {
constructor() {
this.greeting = "Hello";
this.cntr = 0;
}
async createJob(params) {
log(`createJob: ${this.greeting}`);
++this.cntr;
return delay(200);
}
async getJob(jobName) {
log(`getJob: ${this.greeting}`);
++this.cntr;
return delay(100);
}
}
const t = new Transcribe();
log("begin");
chain(t, ["getJob", "createJob"]).createJob().getJob().then(() => {
log(`cntr = ${t.cntr}`);
log("end");
});
Since this returns an actual promise (with additional methods attached), you can directly use .then() or await with it without the separate ._promise() that the first implementation required.
So, you can now do something like this:
const t = new Transcribe();
chain(t, ["getJob", "createJob"]).createJob().getJob().then(() => {
log(`cntr = ${t.cntr}`);
});
or:
const t = new Transcribe();
await chain(t, ["getJob", "createJob"]).createJob().getJob();
log(`cntr = ${t.cntr}`);
And, here's a third version where it creates a thenable object (a pseudo-promise) with the added methods on it (if it bothers you to add methods to an existing promise):
function chain(orig, methodsArray) {
if (!methodsArray || !methodsArray.length) {
throw new Error("methodsArray argument must be array of chainable method names");
}
let masterP = Promise.resolve();
function makeThenable() {
let obj = {};
for (const m of methodsArray) {
obj[m] = function(...args) {
// chain onto master promise to force sequencing
masterP = masterP.then(result => {
return orig[m].apply(orig, ...args);
});
return makeThenable();
}
}
obj.then = function(onFulfill, onReject) {
return masterP.then(onFulfill, onReject);
}
obj.catch = function(onReject) {
return masterP.catch(onReject);
}
obj.finally = function(onFinally) {
return masterP.finally(onFinally);
}
return obj;
}
return makeThenable();
}
function delay(t) {
return new Promise(resolve => {
setTimeout(resolve, t);
});
}
function log(...args) {
if (!log.start) {
log.start = Date.now();
}
const delta = Date.now() - log.start;
const deltaPad = (delta + "").padStart(6, "0");
console.log(`${deltaPad}: `, ...args)
}
class Transcribe {
constructor() {
this.greeting = "Hello";
this.cntr = 0;
}
async createJob(params) {
log(`createJob: ${this.greeting}`);
++this.cntr;
return delay(200);
}
async getJob(jobName) {
log(`getJob: ${this.greeting}`);
++this.cntr;
return delay(100);
}
}
const t = new Transcribe();
log("begin");
chain(t, ["getJob", "createJob"]).createJob().getJob().then(() => {
log(`cntr = ${t.cntr}`);
log("end");
});
The value of videoProgress is not returned in the final object document, i want videoProgress to be returned along with the documentList item, but i am not getting it, i have set item.videoProgress too but not getting it in response:
const document = await Promise.all(documentList.map(async (item) => {
const videoCount = await studentProgressModel.find({ gradeId: item.gradeId, userId : item._id, type: 'VIDEO'}).countDocuments();
const totalVideoCount = await videoModel.find({ gradeId: item.gradeId, mediumId: item.mediumId, status: 'ACTIVE' }).countDocuments();
let videoPercentage = 0;
if(totalVideoCount > 0) {
videoPercentage = (100 * videoCount) / totalVideoCount;
}
item.videoProgress = videoPercentage;
console.log('item', item);
return item;
}));
The issue here is not what anyone in the comments above is discussing, the issue is that you are not actually returning a promise from your documentList.map, but instead are just writing an async function there. Array.map will not wait for any asynchronous behavior, so as soon as it hits an await it returns an empty result.
This can be fixed by instead returning promises in your map:
const document = await Promise.all(documentList.map((item) => { // Removed async here, map is synchronous
// Here we *immediately* return a promise so that map can return that to the promise.all
return new Promise(async (resolve) => {
// Now we can do all our async stuff and promise.all will wait for it :)
const videoCount = await studentProgressModel.find({ gradeId: item.gradeId, userId : item._id, type: 'VIDEO'}).countDocuments();
const totalVideoCount = await videoModel.find({ gradeId: item.gradeId, mediumId: item.mediumId, status: 'ACTIVE' }).countDocuments();
let videoPercentage = 0;
if(totalVideoCount > 0) {
videoPercentage = (100 * videoCount) / totalVideoCount;
}
item.videoProgress = videoPercentage;
console.log('item', item);
return resolve(item); // have to use resolve here with our result to resolve the promise.
});
}));
There are other ways to do this that don't involve an explicit new-promise wrapper, but I figured it made the promise behavior more obvious
Edit: Here's an example which can be run directly here in your browser, I have only replaced the queries with async random number generators.
function randomInteger() {
return new Promise((resolve) => {
setTimeout(() => {
return resolve(Math.floor(Math.random() * 100));
}, 1);
});
}
async function main() {
let documentList = [{}, {}, {}];
const document = await Promise.all(documentList.map((item) => {
return new Promise(async (resolve) => {
const videoCount = await randomInteger();
const totalVideoCount = await randomInteger();
let videoPercentage = 0;
if(totalVideoCount > 0) {
videoPercentage = (100 * videoCount) / totalVideoCount;
}
item.videoProgress = videoPercentage;
console.log('item', item);
return resolve(item);
});
}));
console.log('Document results:', document);
}
main();
I'm writing a wrapper class hiding the internals of working with AudioWorklet. Working with a worklet involves communication between a node and a processor through message ports.
As soon as the code running in the node reaches port.postMessage(), script execution in the node ends. When node.port.onmessage fires (through processor.port.postMessage), code in the node can resume execution.
I can get it to work by using a callback function. See the code below.
class HelloWorklet {
constructor(audioContext) {
audioContext.audioWorklet.addModule('helloprocessor.js').then(() => {
this.awNode = new AudioWorkletNode(audioContext, 'hello-processor');
this.awNode.port.onmessage = (event) => {
switch (event.data.action) {
case 'response message':
this.respondMessage(event.data);
break;
}
}
});
}
requestMessage = (callback) => {
this.awNode.port.postMessage({action: 'request message'});
this.callback = callback;
}
respondMessage = (data) => {
// some time consuming processing
let msg = data.msg + '!';
this.callback(msg);
}
}
let audioCtx = new AudioContext();
let helloNode = new HelloWorklet(audioCtx);
const showMessage = (msg) => {
// additional processing
console.log(msg);
}
const requestMessage = () => {
helloNode.requestMessage(showMessage);
}
and the processor
class HelloProcessor extends AudioWorkletProcessor {
constructor() {
super();
this.port.onmessage = (event) => {
switch (event.data.action) {
case 'request message':
this.port.postMessage({action: 'response message', msg: 'Hello world'});
break;
}
}
}
process(inputs, outputs, parameters) {
// required method, but irrelevant for this question
return true;
}
}
registerProcessor('hello-processor', HelloProcessor);
Calling requestMessage() causes Hello world! to be printed in the console. As using callbacks sometimes decreases the readability of the code, i'd like to rewrite the code using await like so:
async requestMessage = () => {
let msg = await helloNode.requestMessage;
// additional processing
console.log(msg);
}
Trying to rewrite the HelloWorklet.requestMessage I cannot figure out how to glue the resolve of the Promise to the this.awNode.port.onmessage. To me it appears as if the interruption of the code between this.awNode.port.postMessage and this.awNode.port.onmessage goes beyond a-synchronicity.
As using the AudioWorklet already breaks any backwards compatibility, the latest ECMAScript features can be used.
edit
Thanks to part 3 of the answer of Khaled Osman I was able to rewrite the class as follows:
class HelloWorklet {
constructor(audioContext) {
audioContext.audioWorklet.addModule('helloprocessor.js').then(() => {
this.awNode = new AudioWorkletNode(audioContext, 'hello-processor');
this.awNode.port.onmessage = (event) => {
switch (event.data.action) {
case 'response message':
this.respondMessage(event.data);
break;
}
}
});
}
requestMessage = () => {
return new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
this.awNode.port.postMessage({action: 'request message'});
})
}
respondMessage = (data) => {
// some time consuming processing
let msg = data.msg + '!';
this.resolve(msg);
}
}
let audioCtx = new AudioContext();
let helloNode = new HelloWorklet(audioCtx);
async function requestMessage() {
let msg = await helloNode.requestMessage();
// additional processing
console.log(msg);
}
I think there're three things that might help you
Promises don't return multiple values, so something like request message can not be fired again once its fulfilled/resolved, so it won't be suitable to request/post multiple messages. For that you can use Observables or RxJS
You can use util.promisify to convert NodeJS callback style functions to promises like so
const { readFile } = require('fs')
const { promisify } = require('util')
const readFilePromise = promisify(fs.readFile)
readFilePromise('test.txt').then(console.log)
or manually create wrapper functions that return promises around them that resolve/reject inside the callbacks.
For resolving a promise outside of the promise's block you can save the resolve/reject as variables and call them later like so
class MyClass {
requestSomething() {
return new Promise((resolve, reject) => {
this.resolve = resolve
this.reject = reject
})
}
onSomethingReturned(something) {
this.resolve(something)
}
}
At the moment I am using this code below to get the results of several Promises using async await:
let matchday = await createMatchday(2018, 21, [/*9 matches of matchday*/]);
//Further calculations
async function createMatchday(seasonNr, matchdayNr, matches) {
let md = new Matchday(seasonNr, matchdayNr, matches);
await md.getStandings(seasonNr, matchdayNr);
return md;
}
class Matchday {
constructor(seasonNr, matchdayNr, matches) {
this.seasonNr = seasonNr;
this.matchdayNr = matchdayNr;
this.matches = matches;
}
async getStandings(seasonNr, matchdayNr) {
let promiseArr = [];
promiseArr.push(makeHttpRequestTo(`http://externService.com/standings?seasonNr=${seasonNr}&matchdayNr=${matchdayNr}`);
promiseArr.push(makeHttpRequestTo(`http://externService.com/homestandings?seasonNr=${seasonNr}&matchdayNr=${matchdayNr}`));
promiseArr.push(makeHttpRequestTo(`http://externService.com/awaystandings?seasonNr=${seasonNr}&matchdayNr=${matchdayNr}`));
promiseArr.push(makeHttpRequestTo(`http://externService.com/formstandings?seasonNr=${seasonNr}&matchdayNr=${matchdayNr}`));
let resulArr = await Promise.all(promiseArr);
this.standings = resultArr[0];
this.homeStandings = resultArr[1];
this.awayStandings = resultArr[2];
this.formStandings = resultArr[3];
}
}
function makeHttpRequest(url) {
return new Promise((resolve, reject) => {
//AJAX httpRequest to url
resolve(httpRequest.responseText);
}
}
Is this actually the best way to read the values of several promises where the promises don't need to wait for each other to end but rather work at the same time by using Promise.all() or is there a better way to make e.g. several httpRequests at the same time because this seems quite repetetive?
Your URLs all follow the same sort of pattern, so you can greatly reduce your code by mapping an array of ['', 'home', 'away', 'form'] to the URLs. Then, map those URLs to Promises through makeHttpRequestTo, and then you can destructure the awaited results into the this. properties:
async getStandings(seasonNr, matchdayNr) {
const urls = ['', 'home', 'away', 'form']
.map(str => `http://externService.com/${str}standings?seasonNr=${seasonNr}&matchdayNr=${matchdayNr}`);
const promiseArr = urls.map(makeHttpRequestTo);
[
this.standings,
this.homeStandings,
this.awayStandings,
this.formStandings
] = await Promise.all(promiseArr);
}
To populate each property individually rather than waiting for all responses to come back:
async getStandings(seasonNr, matchdayNr) {
['', 'home', 'away', 'form']
.forEach((str) => {
const url = `http://externService.com/${str}standings?seasonNr=${seasonNr}&matchdayNr=${matchdayNr}`;
makeHttpRequestTo(url)
.then((resp) => {
this[str + 'Standings'] = resp;
});
});
}
If you don't want to wait for all requests to complete before continuing the execution flow, you could make the properties of the class be promises:
class Matchday {
constructor(seasonNr, matchdayNr, matches) {
this.seasonNr = seasonNr;
this.matchdayNr = matchdayNr;
this.matches = matches;
['standings', 'homeStandings', 'awayStandings', 'formStandings'].forEach(propertyName => {
let url = `http://externService.com/${propertyName.toLowerCase()}`
+ `?seasonNr=${seasonNr}&matchdayNr=${matchdayNr}`
this[propertyName] = makeHttpRequestTo(url)
});
}
}
Test using the following snippet
class Matchday {
constructor(seasonNr, matchdayNr, matches) {
this.seasonNr = seasonNr;
this.matchdayNr = matchdayNr;
this.matches = matches;
['standings', 'homeStandings', 'awayStandings', 'formStandings'].forEach(propertyName => {
let url = `http://externService.com/${propertyName.toLowerCase()}`
+ `?seasonNr=${seasonNr}&matchdayNr=${matchdayNr}`
this[propertyName] = makeHttpRequestTo(url)
});
}
}
/**************************************
* Test harness
**************************************/
function makeHttpRequestTo(url) {
// Fake an AJAX httpRequest to url
const requested_resource = url.match('^.*\/\/.*\/([^?]*)')[1];
const fake_response_data = 'data for ' + url.match('^.*\/\/.*\/(.*)$')[1];
let delay = 0;
let response = '';
switch (requested_resource) {
// To make it interesting, let's give the 'standings' resource
// a much faster response time
case 'standings':
delay = 250;
break;
case 'homestandings':
delay = 2000;
break;
case 'awaystandings':
delay = 3000;
break;
case 'formstandings':
delay = 4000; // <== Longest request is 4 seconds
break;
default:
throw (util.format('Unexpected requested_resource: %s', requested_resource));
}
return new Promise((resolve, reject) => {
setTimeout(() => resolve(fake_response_data), delay);
});
}
async function testAccessingAllProperties() {
const testId = "Test accessing all properties";
console.log('\n%s', testId);
console.time(testId)
let md = new Matchday(2018, 21, []);
console.log(await md.standings);
console.log(await md.homeStandings);
console.log(await md.awayStandings);
console.log(await md.formStandings);
console.timeEnd(testId)
}
async function testAccessingOnlyOneProperty() {
const testId = `Test accessing only one property`;
console.log('\n%s', testId);
console.time(testId)
let md = new Matchday(2018, 21, []);
console.log(await md.standings);
console.timeEnd(testId)
}
async function all_tests() {
await testAccessingAllProperties();
await testAccessingOnlyOneProperty();
}
all_tests();
Conclusion
The above snippet shows that the execution time is not penalized by properties that are not accessed. And execution time of accessing all properties is no worse than using promise.all.
You'd just need to remember to use await when accessing those properties.
To answer, No you shouldn't block other XHR or any I/O request which are not dependent on each other. I would have written your function like this;
const getFavourites = async () => {
try {
const result = await Promise.resolve("Pizza");
console.log("Favourite food: " + result);
} catch (error) {
console.log('error getting food');
}
try {
const result = await Promise.resolve("Monkey");
console.log("Favourite animal: " + result);
} catch (error) {
console.log('error getting animal');
}
try {
const result = await Promise.resolve("Green");
console.log("Favourite color: " + result);
} catch (error) {
console.log('error getting color');
}
try {
const result = await Promise.resolve("Water");
console.log("Favourite liquid: " + result);
} catch (error) {
console.log('error getting liquid');
}
}
getFavourites();
This way every async functions will be called at once, and no async action will block the other action.
To create a Promise you need to call new Promise((resolve, reject) => { return "Pizza"; })
You're doing it the right way
If you want you can shorten the code by using an array (and its functions like map, etc...), but it won't improve its performance
I am trying to build a way to create a generator which can yield DOM events. More generally, I want to create a way to convert an event system to an async system yielding events.
My initial code example works, but I can see an issue with lifting the resolve function from the Promise so that I can call that function once the event comes in.
class EventPropagation {
constructor(id) {
const button = document.getElementById(id);
let _resolve;
button.addEventListener("click", event => {
if (_resolve) {
_resolve(event);
}
});
let _listen = () => {
return new Promise(resolve => {
_resolve = resolve;
});
}
this.subscribe = async function*() {
const result = await _listen();
yield result;
yield * this.subscribe();
}
}
}
async function example() {
const eventPropagation = new EventPropagation("btn");
for await (const event of eventPropagation.subscribe()) {
console.log(event);
}
}
// call the example function
example();
My question is: Is there a better way of building something like this? There are a lot of things to think about, like multiple events coming in at the same time or cleaning up the listener and the subscriptions. My goal is not to end up with a reactive library but I do want to create small transparent functions which yield events asynchronously.
fiddle
Edited 14 dec 2017 (Edited in response to Bergi's comment)
Async Generators
Babel and a few plugins later; async generators aren't a problem:
const throttle = ms => new Promise(resolve => setTimeout(resolve, ms));
const getData = async() => {
const randomValue = Math.floor(Math.random() * 5000 + 1);
await throttle(randomValue);
return `The random value was: ${randomValue}`;
}
async function* asyncRandomMessage() {
const message = await getData();
yield message;
// recursive call
yield *asyncRandomMessage();
}
async function example() {
for await (const message of asyncRandomMessage()) {
console.log(message);
}
}
// call it at your own risk, it does not stop
// example();
What I want to know is how I transform a series of individual callback calls into an async stream. I can't imagine this problem isn't tackled. When I look at the library Bergi showed in the comments I see the same implementation as I did, namely: "Store the resolve and reject functions somewhere the event handler can call them." I can't imagine that would be a correct way of solving this problem.
You need a event bucket, here is an example:
function evtBucket() {
const stack = [],
iterate = bucket();
var next;
async function * bucket() {
while (true) {
yield new Promise((res) => {
if (stack.length > 0) {
return res(stack.shift());
}
next = res;
});
}
}
iterate.push = (itm) => {
if (next) {
next(itm);
next = false;
return;
}
stack.push(itm);
}
return iterate;
}
;(async function() {
let evts = evtBucket();
setInterval(()=>{
evts.push(Date.now());
evts.push(Date.now() + '++');
}, 1000);
for await (let evt of evts) {
console.log(evt);
}
})();
My best solution thus far has been to have an internal EventTarget that dispatches events when new events are added onto a queue array. This is what I've been working on for a JS modules library (including used modules here). I don't like it... But it works.
Note: This also handles the new AbortSignal option for event listeners in multiple places.
export function isAborted(signal) {
if (signal instanceof AbortController) {
return signal.signal.aborted;
} else if (signal instanceof AbortSignal) {
return signal.aborted;
} else {
return false;
}
}
export async function when(target, event, { signal } = {}) {
await new Promise(resolve => {
target.addEventListener(event, resolve, { once: true, signal });
});
}
export async function *yieldEvents(what, event, { capture, passive, signal } = {}) {
const queue = [];
const target = new EventTarget();
what.addEventListener(event, event => {
queue.push(event);
target.dispatchEvent(new Event('enqueued'));
}, { capture, passive, signal });
while (! isAborted(signal)) {
if (queue.length === 0) {
await when(target, 'enqueued', { signal }).catch(e => {});
}
/**
* May have aborted between beginning of loop and now
*/
if (isAborted(signal)) {
break;
} else {
yield queue.shift();
}
}
}
The example provided by NSD, but now in Typescript
class AsyncQueue<T> {
private queue: T[] = [];
private maxQueueLength = Infinity;
private nextResolve = (value: T) => {};
private hasNext = false;
constructor(maxQueueLength?: number) {
if (maxQueueLength) {
this.maxQueueLength = maxQueueLength;
}
}
async *[Symbol.asyncIterator]() {
while (true) {
yield new Promise((resolve) => {
if (this.queue.length > 0) {
return resolve(this.queue.shift());
}
this.nextResolve = resolve;
this.hasNext = true;
});
}
}
push(item: T) {
if (this.hasNext) {
this.nextResolve(item);
this.hasNext = false;
return;
}
if (this.queue.length > this.maxQueueLength) {
this.queue.shift();
}
this.queue.push(item);
}
}
(async function () {
const queueu = new AsyncQueue<string>();
setInterval(() => {
queueu.push(Date.now().toString());
queueu.push(Date.now().toString() + "++");
}, 1000);
for await (const evt of queueu) {
console.log(evt);
}
})();