How do I build an API that lets me do `await t.a().b()` & `await t.a(); await t.b()`?

I want to build an API that lets me chain in two ways:
await t
.a()
.b()
.c()
...
// or
const t = ...;
await t.a();
await t.b();
await t.c();
...
await t
.a()
.b()
.c()
...
// or
const t = ...;
await t.a();
await t.b();
await t.c();
...
This is known as the builder pattern. Usually it's straightforward, but I'm not sure how to create one when promises are involved. NOTE: This is the way https://testcafe.io/ 's api works. I'm trying to build a similar api. See https://discord.com/channels/436251713830125568/1158962809401516062/1159217101471481896 (links to a few messages down)
21 Replies
dys 🐙
dys 🐙14mo ago
You are wanting to call a() and have it return a Promise or an object with a b function deepending on how it was called. (The "dot" (.) operator is higher precedence than await, so await a().b().c() === await (a().b().c())) . There is no way within a function call to know whether the returning context is expecting a Promise or not, so you can't do what you're describing. I mean you could return a Promise from a() that has a b method, but you need to wait for a() to resolve before you'd have a value to do anything with in b, and you're not waiting in a().b().c(). (Note that the then() method of a Promise is called with the result of the Promise & accepts a Promise as output, but that's a special case.) Are you talking about the two code examples on https://testcafe.io? They're two different frameworks: Testcase & Selenium, not different examples of a single framework. If you're not taking about those pieces of code, where are you seeing these alternate syntaxes?
Bawdy Ink Slinger
Bawdy Ink SlingerOP14mo ago
@dys 🐙 I'm only referring to the left hand side. It doesn't show it being called like in my second example, but I know it works both ways because I've got many examples in my project EDIT: Oops. I was wrong. I've updated my OP
There is no way within a function call to know whether the returning context is expecting a Promise or not, so you can't do what you're describing.
I believe the library returns a promise that's been assigned other functions to make it so the code doesn't know how its being called; the single return can handle both scenarios
Jochem
Jochem14mo ago
Dysbulic is correct, what you're describing isn't possible. a can either return a promise or an object with other methods, not both. If it returns a promise, chaining won't work without awaiting the promise first. If it returns an object, awaiting it in the second example will do nothing
Rägnar O'ock
Rägnar O'ock14mo ago
You could maybe do it if the b() and c() methods internally call this.then() or await this. But I'm not sure if it would actually work.
Jochem
Jochem14mo ago
I don't think you can know whether a method is being called with await in the method itself though
Rägnar O'ock
Rägnar O'ock14mo ago
You don't need to You can call .then() on a promise that has already been fulfilled So you just do it every time the function is called and use the resulting promise/value in the body of the method
Jochem
Jochem14mo ago
it feels like a terrible idea overall though, to have a method that returns Schrodinger's Promise
Rägnar O'ock
Rägnar O'ock14mo ago
What do you mean by "Schrodinger's Promise" ? A promise that is maybe already fulfilled?
Jochem
Jochem14mo ago
effectively yeah
Rägnar O'ock
Rägnar O'ock14mo ago
'Cause that's what promises are for, doing stuff after some other stuff has finished running.
Jochem
Jochem14mo ago
regardless, it makes the await pointless, right? Cause you can call it or not call it (on a() and b()), and the result is the same
Rägnar O'ock
Rägnar O'ock14mo ago
Yes But maybe the results of a() has other stuff they are interested in And actually the first .b() and the second aren't even called on the same object. One's on the promise the other on the value inside the promise so you don't even need to have the same implementation in both. They just need to have the same signature for it to work (a === await a // false) I'll try something when I get home
Bawdy Ink Slinger
Bawdy Ink SlingerOP14mo ago
a can either return a promise or an object with other methods, not both
const promise = Promise.resolve().then(() => console.log('then'));
const both = Object.assign(promise, { myFun: () => console.log('myFun') });
both.myFun();
await both;

// myFun
// then
const promise = Promise.resolve().then(() => console.log('then'));
const both = Object.assign(promise, { myFun: () => console.log('myFun') });
both.myFun();
await both;

// myFun
// then
The above runs. Is this different from what you mean?
Jochem
Jochem14mo ago
hm, I guess not. But that feels unbelievably cursed
Bawdy Ink Slinger
Bawdy Ink SlingerOP14mo ago
agreed. I didn't know it was possible until I tried to copy this API but the API is really great, even if the impl is weird actually let me test something okay I've been mistaken
const xxx = await t
.click(Selector('.passage button').withText('a'))
.expect(Selector('.flash-message').innerText)
.eql('a')

await xxx
.click(Selector('.passage button').withText('b'))
.expect(Selector('.flash-message').innerText)
.eql('b')
.click(Selector('.passage button').withText('c'))
.expect(Selector('.flash-message').innerText)
.eql('c')

// TypeError: Cannot read properties of undefined (reading 'click')
const xxx = await t
.click(Selector('.passage button').withText('a'))
.expect(Selector('.flash-message').innerText)
.eql('a')

await xxx
.click(Selector('.passage button').withText('b'))
.expect(Selector('.flash-message').innerText)
.eql('b')
.click(Selector('.passage button').withText('c'))
.expect(Selector('.flash-message').innerText)
.eql('c')

// TypeError: Cannot read properties of undefined (reading 'click')
I guess I've never tried the above, I only thought I had. But this works:
await t
.click(Selector('.passage button').withText('a'))
.expect(Selector('.flash-message').innerText)
.eql('a')

await t
.click(Selector('.passage button').withText('b'))
.expect(Selector('.flash-message').innerText)
.eql('b')
.click(Selector('.passage button').withText('c'))
.expect(Selector('.flash-message').innerText)
.eql('c')
await t
.click(Selector('.passage button').withText('a'))
.expect(Selector('.flash-message').innerText)
.eql('a')

await t
.click(Selector('.passage button').withText('b'))
.expect(Selector('.flash-message').innerText)
.eql('b')
.click(Selector('.passage button').withText('c'))
.expect(Selector('.flash-message').innerText)
.eql('c')
I suppose that's much easier to implement updated my OP. Still not sure how to do this, either
Rägnar O'ock
Rägnar O'ock14mo ago
(don't ask me what it does or how it does it... I don't understand it either as for https://discord.com/channels/436251713830125568/1158962809401516062/1159219191400890428 you have the have your t, t.a() and t.b() either be the same object (by doing return this in every method) or implement the same interface (basically same as before but the object that gets returned is not the same as the one that was called upon)
Bawdy Ink Slinger
Bawdy Ink SlingerOP14mo ago
@Rägnar O'ock thanks! I'll check these out soon
dys 🐙
dys 🐙14mo ago
If you're unaware, Discord supports Markdown links now.
Rägnar O'ock
Rägnar O'ock14mo ago
I am aware... but there's no point in hiding those links
Want results from more Discord servers?
Add your server