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");
});
Currently I am doing sth like this:
declare type Resource = { dispose: () => void }
declare function getResource(): Promise<Resource>
async function withResource<T>(executor: (r: Resource) => T) {
const r = await getResource()
try {
return await executor(r)
} finally {
r.dispose()
}
}
...
withResource(async r => {
// code goes here
})
Is there a better way to achieve this behavior ?
I would like to be able to .then() a instantiated object while following the Promise standards.
Or is this not recommended?
I tried the following but I don't think it's the right approach...
class MyClass extends Promise {
constructor(){
this.loaded = false;
//Non promise third-party callback async function
someAsyncFunction( result => {
this.loaded = true;
this.resolve(result);
}
}
}
const myClass = new MyClass();
myClass.then( result => {
console.log(result);
console.log(myClass.loaded);
// >>true
})
Edit:
What I ended up doing was the following, but I'm not sure about using .load().then()
class MyClass {
constructor(){
this.loaded = false;
}
load(){
return new Promise( resolve => {
//Non promise third-party callback async function
someAsyncFunction( result => {
this.loaded = true;
resolve(result);
}
})
}
}
const myClass = new MyClass();
myClass.load().then( result => {
console.log(result);
console.log(myClass.loaded);
// >>true
})
You can have custom then-able objects, but your intent is not quite clear. If the code should ensure that the instance of MyClass is ready before you use it, then you should use either a factory function returning that object as soon as it is ready, or if certain functions depend on async loading make those functions async too.
The then-able object does not prevent you from using before it was resolved, so that design does not help you with maintainability or error safety.
Factory function:
function createMyClass(options) {
const myClass = new MyClass();
return loadData(options).then( (result) => {
myClass.loaded = true;
myClass.result = result;
return myClass;
})
}
createMyClass({/*some options*/}).then( myClass => {
console.log(myClass.result);
console.log(myClass.loaded);
})
Load the result on demand:
class MyClass {
constructor(options) {
this.loaded = false;
this.options = options;
}
result() {
// only request the data if it was not already requested
if (!this._result) {
this._result = loadData(this.options).then(result => {
this.loaded = true
return result
});
}
return this._result
}
}
var myClass = new MyClass({/*....*/})
myClass.result().then(result => {
console.log(result)
})
// could be called another time, and the data is not requested over again,
// as the Promise is reused
myClass.result().then(result => {
console.log(result)
})
Or is this not recommended?
Not only is this not recommended but it will also never work.
You should only use the constructor to define initial state values or
perform construction value validations etc.
You can use an init() method to do what you want like so:
class MyClass {
constructor(){
this.loaded = false
}
init() {
return someAsyncFunction()
.then(value => {
this.loaded = true
return value
})
}
}
This is how you can write the promise
const someAsyncFunction = (parameters) => {
return new Promise((resolve, reject) => {
if (success) {
resolve();
} else {
reject();
}
});
};
someAsyncFunction
.then((result) => {
})
.catch((err) => {
});
I am currently working on a file uploading method which requires me to limit the number of concurrent requests coming through.
I've begun by writing a prototype to how it should be handled
const items = Array.from({ length: 50 }).map((_, n) => n);
from(items)
.pipe(
mergeMap(n => {
return of(n).pipe(delay(2000));
}, 5)
)
.subscribe(n => {
console.log(n);
});
And it did work, however as soon as I swapped out the of with the actual call. It only processes one chunk, so let's say 5 out of 20 files
from(files)
.pipe(mergeMap(handleFile, 5))
.subscribe(console.log);
The handleFile function returns a call to my custom ajax implementation
import { Observable, Subscriber } from 'rxjs';
import axios from 'axios';
const { CancelToken } = axios;
class AjaxSubscriber extends Subscriber {
constructor(destination, settings) {
super(destination);
this.send(settings);
}
send(settings) {
const cancelToken = new CancelToken(cancel => {
// An executor function receives a cancel function as a parameter
this.cancel = cancel;
});
axios(Object.assign({ cancelToken }, settings))
.then(resp => this.next([null, resp.data]))
.catch(e => this.next([e, null]));
}
next(config) {
this.done = true;
const { destination } = this;
destination.next(config);
}
unsubscribe() {
if (this.cancel) {
this.cancel();
}
super.unsubscribe();
}
}
export class AjaxObservable extends Observable {
static create(settings) {
return new AjaxObservable(settings);
}
constructor(settings) {
super();
this.settings = settings;
}
_subscribe(subscriber) {
return new AjaxSubscriber(subscriber, this.settings);
}
}
So it looks something like this like
function handleFile() {
return AjaxObservable.create({
url: "https://jsonplaceholder.typicode.com/todos/1"
});
}
CodeSandbox
If I remove the concurrency parameter from the merge map function everything works fine, but it uploads all files all at once. Is there any way to fix this?
Turns out the problem was me not calling complete() method inside AjaxSubscriber, so I modified the code to:
pass(response) {
this.next(response);
this.complete();
}
And from axios call:
axios(Object.assign({ cancelToken }, settings))
.then(resp => this.pass([null, resp.data]))
.catch(e => this.pass([e, null]));
How can I implement this promise scenario in ng2?
export class MySpecializedClass
{
myObject;
constructor(
private myService: MyService
)
{
this.myObject = new MyObject();
}
buildMyObject()
{
this.builderMethod1();
this.builderMethod2();
}
builderMethod1()
{
this.myService.getData1()
.then(response => this.myObject.Prop1 = response.Prop1 )
}
builderMethod2()
{
this.myService.getData2()
.then(response => this.myObject.Prop2 = response.Prop2 )
}
}
export class MyConsumerClass
{
myObect;
getMyObject()
{
this.myObject = new MySpecializedClass().buildMyObject().myObject;
}
}
The problem is that in the following line of code, myObject should not be referenced until builderMethod1() and builderMethod2() are guaranteed complete.
MyConsumerClass.getMyObject().myObject
I want builderMethod1() and builderMethod2() to run at the same time which is why they aren't chained in a then(). How could I implement this scenario with Promises? Or would Observables or a different approach provide a better solution?
Personally I find the async-await syntax a lot more readable than then chaining. Maybe you will too, this is how the same thing can be done using async-await:
export class MySpecializedClass
{
myObject : MyObject;
constructor(
private myService: MyService
)
{
this.myObject = new MyObject();
}
async buildMyObject()
{
const first = this.builderMethod1();
await this.builderMethod2();
await first;
return this.myObject;
}
async builderMethod1()
{
const response = await this.myService.getData1();
this.myObject.Prop1 = response.Prop1;
}
async builderMethod2()
{
const response = await this.myService.getData2();
this.myObject.Prop2 = response.Prop2;
}
}
export class MyConsumerClass
{
myObject;
async getMyObject()
{
this.myObject = await new MySpecializedClass(new MyService()).buildMyObject()
}
}
You can just return the promise:
builderMethod2()
{
return this.myService.getData2()
.then(response => {
this.myObject.Prop2 = response.Prop2;
return response.Prop2;
})
}
You can do this in both promise and Observable. I would prefer observable as it have more options over promise
promise
In case of promise, you need to return promise/differ. But in your case you are not returning anything and also when you say .then it resolves the promise
export class MySpecializedClass
{
myObject;
constructor(
private myService: MyService
)
{
this.myObject = new MyObject();
}
buildMyObject()
{
var diff = differed // find out exact differed object
return diff.all(this.builderMethod1(), this.builderMethod2());
}
builderMethod1()
{
return this.myService.getData1()
.then(response => this.myObject.Prop1 = response.Prop1 )
}
builderMethod2()
{
return this.myService.getData2()
.then(response => this.myObject.Prop2 = response.Prop2 )
}
}
export class MyConsumerClass
{
myObect;
getMyObject()
{
this.myObject = new MySpecializedClass().buildMyObject().then (() => {
myObject;
}
}
}
Observable
In case of observable you can you Observable.merger, Observable.formJoin etc to join 2 of your observable and subscribe them