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");
});
Related
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();
// ...
}
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 using node-serialport to communicate with a piece of hardware. It just writes a command and receives a response.
https://serialport.io/docs/en/api-parsers-overview
The following code works:
const port = new SerialPort(path);
const parser = port.pipe(new Readline({ delimiter: '\r', encoding: 'ascii' }));
const requestArray = [];
parser.on('data', (data) => {
// get first item in array
const request = requestArray[0];
// remove first item
requestArray.shift();
// resolve promise
request.promise.resolve(data);
});
export const getFirmwareVersion = async () => {
let resolvePromise;
let rejectPromise;
const promise = new Promise((resolve, reject) => {
resolvePromise = resolve;
rejectPromise = reject;
});
const title = 'getFirmwareVersion';
const cmd = 'V\r';
requestArray.push({
title,
cmd,
promise: {
resolve: resolvePromise,
reject: rejectPromise
}
});
await v2Port.write(cmd);
return promise;
};
Then from my app (which is written in electron/react) I can call the function:
<Button onClick={() => {
let data = await _api.getFirmwareVersion();
console.log('done waiting...');
console.log(data);
}>
Click Me
</Button>
Is there anyway I can refactor this code to make it more succinct?
Is there a way to get the Promise from the async function, rather than having to make a new Promise?
Is there a way to tap into the Transform Stream that already exists and pipe the Promise in there somehow?
I'm also new to async/await, and wanted to avoid using callbacks, especially in the React/Redux side of things.
I aim to have a lot of these endpoints for the api (i.e. getFirmwareVersion, getTemperature, etc...). So I want to make the code as concise as possible. I don't want the UI to have any underlying knowledge of how the API is getting the data. It just needs to request it like any other API and wait for a response.
Oh, I think I get it. The parser is receiving data constantly. So when a request comes, you wait for the next data and send it when it arrives. I suggest you to write an intermediate class.
Like this:
const SerialPort = require('serialport')
const Readline = require('#serialport/parser-readline')
const { EventEmitter } = require('events');
class SerialPortListener extends EventEmitter {
constructor(path) {
super();
this.serialPortPath = path;
}
init() {
this.serialPort = new SerialPort(this.serialPortPath);
const parser = this.serialPort.pipe(new Readline({ delimiter: '\r', encoding: 'ascii' }));
parser.on('data', data => this.emit('data', data));
}
}
Then you could modify the getFirmwareVersion like this:
const serialPortListener = new SerialPortListener(path);
serialPortListener.init();
export const getFirmwareVersion = () => {
return new Promise((resolve, reject) => {
serialPortListener.once('data', async (data) => {
try {
const cmd = 'V\r';
await v2Port.write(cmd);
resolve(data);
} catch (ex) {
reject(ex);
}
});
});
};
Based on help from Mehmet, here is what I ended up with:
const _port = new SerialPort(path);
const _parser = _port.pipe(new Readline({ delimiter: '\r', encoding: 'ascii' }));
const waitForData = async () => {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => reject('Write Timeout'), 500);
_parser.once('data', (data) => {
clearTimeout(timeoutId);
resolve(data);
});
});
};
const createAPIFunction = (cmdTemplate, validationString) => {
return async (config) => {
try {
// replace {key} in template with config[key] props
const cmd = cmdTemplate.replace(/{(\w+)}/g, (_, key) => {
return config[key];
});
_port.write(cmd + '\r');
const data = await waitForData();
// validate data
if (data.startsWith(validationString)) {
// is valid
return data;
} else {
// invalid data
throw new Error('Invalid Data Returned');
}
} catch (err) {
throw err;
}
};
};
export const getFirmwareVersion = createAPIFunction('V', 'V1');
export const enableSampling = createAPIFunction('G1{scope}', 'G11');
So i have request coming based on that i am building this.request array so i can make calls to urls and request.body that is pushed into array , In below code trying to use RequestResponseHandler.processRequest for both promises but its always going into spec(duplicate call) when i call same function processRequest for other promise , How can i make sure if processRequest is called for spec ignore that if condition and go to PTM
request
{ header: { lineOfBusiness: ["spec","PTM"] } , body: data }
handler.ts
export class RequestResponseHandler {
public static processRequest(data: any, url: string): Promise < any > {
const reqObject: IRequestURL = {}
as IRequestURL;
const lob = data.header.lineOfBusiness;
if (lob[0] === "spec") {
const specUrl = urlConfig + url;
reqObject.url = specUrl;
reqObject.body = data;
}
if (lob[1] === "PTM") {
const ptmUrl = urlConfig + url;
reqObject.url = ptmUrl;
reqObject.body = data;
}
return Promise.resolve(reqObject);
}
}
controllet.ts
const bRetSpec: boolean = await this.specBalanceRequest(request);
const bRetPtm: boolean = await this.ptmBalanceRequest(request);
if (!bRetPtm && !bRetSpec) {
return Promise.reject(new Error("Processing failed"));
}
try {
const __data: IResponse = await makeRequest(this._request);
const resp1 = await another promise to resolve _data[0];
const resp2 = await another promise to resolve _data[1];
return await Promise.all([resp1, resp2]);
} catch (err) {
return Promise.reject(err);
}
private async specBalanceRequest(#Body() request: ExpressRequest): Promise < boolean > {
const specUrl = "/payments";
const reqObject = await RequestResponseHandler.processRequest(request.body, specialtyUrl);
this._request.push(reqObject);
return Promise.resolve(true);
}
private async ptmBalanceRequest(#Body() request: any): Promise < boolean > {
const careURL = "/Order";
const reqObject = await RequestResponseHandler.processRequest(request.body, careURL);
this._request.push(reqObject);
return Promise.resolve(true);
}
One thing that I suggest is to not make unnecessary promise function. Keep it simple as possible.
RequestResponseHandler.processRequest doesn't need to be a promise
export class RequestResponseHandler {
public static processRequest(data: any, url: string): IRequestURL {
const reqObject: IRequestURL = {} as IRequestURL;
const lob = data.header.lineOfBusiness;
// I guess we can improve code below, no diff between "spec" and "ptm"
if (lob[0] === "spec") {
const specUrl = urlConfig + url;
reqObject.url = specUrl;
reqObject.body = data;
}
if (lob[1] === "PTM") {
const ptmUrl = urlConfig + url;
reqObject.url = ptmUrl;
reqObject.body = data;
}
return reqObject; // return value not a promise
}
}
in controller file
const bRetSpec: boolean = this.specBalanceRequest(request); // remove `await`
const bRetPtm: boolean = this.ptmBalanceRequest(request); // remove `await`
if (!bRetPtm && !bRetSpec) {
return Promise.reject(new Error("Processing failed"));
}
try {
const __data: IResponse = await makeRequest(this._request);
// the two promise calls, I guess can be move to be inside promise.all
return await Promise.all([anotherPromiseData0, anotherPromiseData1]);
} catch (err) {
return Promise.reject(err);
}
// make it non promise
private specBalanceRequest(#Body() request: ExpressRequest): void {
const specUrl = "/payments";
const reqObject = RequestResponseHandler.processRequest(request.body, specialtyUrl);
this._request.push(reqObject);
}
// make it non promise
private ptmBalanceRequest(#Body() request: any): void {
const careURL = "/Order";
const reqObject = RequestResponseHandler.processRequest(request.body, careURL);
this._request.push(reqObject);
}
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);
}
})();