refactor: add chainable operations#575
refactor: add chainable operations#575CarolinaMoraes merged 3 commits intofeature/product-403-add-publish-methodfrom
Conversation
| return this.resend.post<CreateTemplateResponseSuccess>( | ||
| '/templates', | ||
| payload, | ||
| ); | ||
| return data; | ||
| } |
There was a problem hiding this comment.
Not sure if this is required for it to work, but I would rather avoid returning a Promise since, without awaiting, if an error occurs, then the caller function won't appear in the stacktrace. See https://github.com/goldbergyoni/nodebestpractices/blob/master/sections/errorhandling/returningpromises.md for more details
There was a problem hiding this comment.
I think that if we change it to return await ... it does the job and keep the stacktrace safe, doesn't it?
There was a problem hiding this comment.
++ to this! If anyone wants more context, this is helpful.
There was a problem hiding this comment.
@emiliosheinz, I didn’t think about it before
Thanks a lot for pointing it out
Unfortunately, for this solution, it needs to return the promise itself
For the chaining to work, for example create().publish(), create() must return an object with a publish method immediately
If we use await, the create() method would be async. This method then would return Promise<ChainableResult> (and ChainableResult already is a Promise-like object), which we wouldn’t be able to chain since Promise<ChainableResult> doesn’t have a publish method, only ChainableResult does
As this is still under the preview branch and the implementation can change a lot until it reaches the general public, I’ll merge this
But I took note of what you and @lucasfcosta pointed out and will make sure to gather more feedback about it (maybe we even change the approach of this automatic publishing to something other than chaining)
There was a problem hiding this comment.
Noted! Thank you for the explanation and for looking into it.
maybe we even change the approach of this automatic publishing to something other than chaining
IMO, simplifying it by implementing the separate publish method that receives and id as you did initially, would probably make it more predictable, easier to use and maintain :)
lucasfcosta
left a comment
There was a problem hiding this comment.
Given we were already talking about traces, I have left a comment about how we could better preserve stack traces when using these PromiseLike objects.
Still, I would not block merging this PR because of that given that it introduces some significant complexity for a situation that probably does not happen often and limited benefits.
| return this.resend.post<CreateTemplateResponseSuccess>( | ||
| '/templates', | ||
| payload, | ||
| ); | ||
| return data; | ||
| } |
There was a problem hiding this comment.
++ to this! If anyone wants more context, this is helpful.
| constructor(private readonly resend: Resend) {} | ||
|
|
||
| async create( | ||
| create( |
There was a problem hiding this comment.
One issue that we might have with these new PromiseLikes is that we can miss stack traces. This is related to @emiliosheinz's comment.
I don't think this is a must but I thought it was worth commenting given other people were talking about traces.
In here, we're creating a PromiseLike so we're not marking the create function as async nor awaiting on the actual creation and thus Node won't have its frame on the stack.
You can run the following test to confirm this behaviour:
it('test async stack trace for create', async () => {
const mockReactComponent = {
type: 'div',
props: { children: 'Welcome!' },
} as React.ReactElement;
// Mock renderAsync to throw an error
mockRenderAsync.mockRejectedValueOnce(new Error('React rendering failed'));
const payload: CreateTemplateOptions = {
name: 'Welcome Email',
react: mockReactComponent,
};
const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop');
// This function will call create and we want to see it in the trace
const createTemplate = async () => {
return await resend.templates.create(payload);
};
// The error should be thrown and the stack trace should include the createTemplate function, but it doesn't
try {
await createTemplate();
fail('Should have thrown an error');
} catch (error) {
// I recommend keeping this log so you can actually copy it and compare
const stack = (error as Error).stack;
console.log('Stack trace:', stack);
expect(error).toBeInstanceOf(Error);
expect((error as Error).message).toContain('React rendering failed');
// This fails because `createTemplate` is not part of the stack at all but it should have been
expect(stack).toContain('createTemplate');
}
});If you do that you will see that the top of your stack trace has the line for React rendering failed (that's what L63 corresponds to for me, but it does not include Templates.create or even createTemplate (L74` on the trace.
at /Users/lucasfcosta/repositories/resend-node/src/templates/templates.spec.ts:63:45
at Generator.next (<anonymous>)
at /Users/lucasfcosta/repositories/resend-node/src/templates/templates.spec.ts:8:71
at new Promise (<anonymous>)
at Object.<anonymous>.__awaiter (/Users/lucasfcosta/repositories/resend-node/src/templates/templates.spec.ts:4:12)
at Object.<anonymous> (/Users/lucasfcosta/repositories/resend-node/src/templates/templates.spec.ts:56:56)
at Promise.then.completed (/Users/lucasfcosta/repositories/resend-node/node_modules/.pnpm/jest-circus@29.7.0/node_modules/jest-circus/build/utils.js:298:28)
<further internals omitted>
However, if you use something like Error.captureStackTrace when creating the object you will be able to capture the stack at that point in time and you can then enrich again when throwing. That way, you will see the actual Templates.create at the top of the stack, as well as its caller.
at Templates.create (/Users/lucasfcosta/repositories/resend-node/src/templates/templates.ts:33:12)
at /Users/lucasfcosta/repositories/resend-node/src/templates/templates.spec.ts:74:39
at Generator.next (<anonymous>)
at /Users/lucasfcosta/repositories/resend-node/src/templates/templates.spec.ts:8:71
at new Promise (<anonymous>)
at Object.<anonymous>.__awaiter (/Users/lucasfcosta/repositories/resend-node/src/templates/templates.spec.ts:4:12)
at createTemplate (/Users/lucasfcosta/repositories/resend-node/src/templates/templates.spec.ts:73:41)
at /Users/lucasfcosta/repositories/resend-node/src/templates/templates.spec.ts:79:15
<further internals omitted>
If we were to capture the trace what we'd have to do is something like:
export class ChainableTemplateResult<
T extends CreateTemplateResponse | DuplicateTemplateResponse,
> implements PromiseLike<T>
{
private readonly stackTraceTarget: any = {};
constructor(
private readonly promise: Promise<T>,
private readonly publishFn: (
id: string,
) => Promise<PublishTemplateResponse>,
) {
// Capture stack trace excluding this constructor for better error reporting
if ('captureStackTrace' in Error) {
Error.captureStackTrace(this.stackTraceTarget, ChainableTemplateResult);
}
}
// ...
then<TResult1 = T, TResult2 = never>(
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null,
): PromiseLike<TResult1 | TResult2> {
return this.promise.then(
onfulfilled,
(reason: unknown) => {
// add some logic to recover the trace, message, and name from the captured trace
}
);
}
// ...
}There was a problem hiding this comment.
Thanks a lot @lucasfcosta for the thorough feedback on this (and also the potential solution to this issue)!!
As this branch is still for internal/beta use and won't be available for the general public, this implementation can still change a lot. So I'll go ahead and merge it
But this review didn't go unnoticed. I'll gather more feedback on this method usage to see whether we want to use this Promise-like approach still, or if we should minimize the complexity doing this another way
There was a problem hiding this comment.
Sounds good @CarolinaMoraes! Thanks for the work on this, I especially liked the elegant typing that went into this 🖤
98dad14
into
feature/product-403-add-publish-method
Description
In order to achieve something like was described here:
A new class
ChainableTemplateResultwas introduced. It uses PromiseLike to support the native Promise behavior while also enhancing the behavior of the Promise by adding thepublishmethod to it.The method
publishcan now be chained after thecreateandduplicatemethods are executed.(Although the chaining is possible, is not required. Users that only use
create()orduplicate()won't be affected)Summary by cubic
Adds chainable template operations so you can publish right after create or duplicate in a single call. This reduces two SDK calls to one and keeps normal await behavior.
New Features
Migration
Related resources
#574