Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support deferred functions with waitUntil #1705

Open
1 task done
nandi95 opened this issue Sep 8, 2023 · 8 comments
Open
1 task done

Support deferred functions with waitUntil #1705

nandi95 opened this issue Sep 8, 2023 · 8 comments
Labels
discussion enhancement New feature or request

Comments

@nandi95
Copy link

nandi95 commented Sep 8, 2023

Describe the feature

I have an endpoint I don't want the user to wait while I talk to third party apis that isn't critical to the response So such logic could be run after the response has been sent.
For example:

// ~/server/api/form.post.ts
import { object, string } from 'yup';
import runAfterResponse from "~/server/utils/runAfterResponse";

export default defineEventHandler(async event => {
    const body = await readValidatedBody(event, body => {
        const bodySchema = object({
            email: string().required().email()
        });

        return bodySchema.validateSync(body);
    });

    const sendEmail = async () => {
        await ...logic;
    };

    // user doesn't really needs to wait for these...
    runAfterResponse(event, async () => ...talkToS3);
    runAfterResponse(event, sendEmail);

    return {
        success: true
    };
});

Then the runAfterResponse looks like this:

// ~/server/utils/runAfterResponse.ts
import type { H3Event } from 'h3';
import getRequestFingerprint from '~/server/utils/getRequestFingerprint';

type Callback = () => void | Promise<void>;
export type CallbackDictionary = Record<string, Callback[]>;

// I'm certain this is less then optimal in large applications but good for mvp
export const currentDict: CallbackDictionary = {};

export default function runAfterResponse(event: H3Event, fn: Callback): void {
    const fingerprint = getRequestFingerprint(event)!;

    if (!(fingerprint in currentDict)) {
        currentDict[fingerprint] = [] as Callback[];
    }

    currentDict[fingerprint].push(fn);
}

getRequestFingerprint in turn looks like:

// ~/server/utils/getRequestFingerprint.ts
import type { H3Event } from 'h3';
import crypto from 'crypto';

export default function getRequestFingerprint(event: H3Event): string {
    return crypto.createHash('sha1')
        .update(`${getRequestIP(event)}-${event.method}-${getRequestURL(event).toString()}`)
        .digest('hex');
}

(the getRequestFingerprint could be a Pull Request on its own...
And lastly I have a nitro plugin that will process these callbacks

// ~/server/plugins/runAfterResponse.ts
import getRequestFingerprint from '~/server/utils/getRequestFingerprint';
import { currentDict } from '~/server/utils/runAfterResponse';

export default defineNitroPlugin((nitroApp) => {
    nitroApp.hooks.hook('afterResponse', async (event) => {
        const fingerprint = getRequestFingerprint(event)!;


        for (const fn of currentDict[fingerprint]) {
            await fn();
        }

        // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
        delete currentDict[fingerprint];

        return;
    });
});

Let me know your thoughts, concerns, etc.
I have tested this and it works as expected with with the order of execution console.log(new Date().getSeconds(), {count})

Additional information

  • Would you be willing to help implement this feature?
@pi0
Copy link
Member

pi0 commented Sep 8, 2023

Hi. I think what you are looking for is event.waitUntil(() => {} it works universally (and makes cloudflare worker to be alive until tasks are being done and response sent).

You can also access to the pending promises with waitUntil inside a nitro plugin by hooking to afterresponse and using event.context.

See this example: https://stackblitz.com/edit/github-ch89s7?file=routes%2Findex.ts,plugins%2Fbackground.ts

Let me know if this is what you think and resolves your problem or if have any idea to improve API.

@pi0 pi0 changed the title After response callback Running tasks after response Sep 8, 2023
@pi0 pi0 added question Further information is requested discussion and removed pending triage labels Sep 8, 2023
@Hebilicious
Copy link
Member

If I'm not mistaken it looks like you are implementing a very basic queue pattern ? I would love if we could support queues as a first class citizen (similar to laravel); in the meantime there's this cloudflare hook based approach we've been tinkering with https://github.com/unjs/nitro/pull/1579/files

@nandi95
Copy link
Author

nandi95 commented Sep 8, 2023

waitUntil does in fact do what I want.

I get that it's called waitUntil because the server will wait until all these functions are finished, but this makes most sense from the library developer's perspective. For me, it would read better if it was called something like afterResponse. My heart isn't set on it I just think that the naming should make most sense from the perspective of a request<->response cycle.

I'm not seeing where these waitUntil promises are resolved? Where do I find that? I'm interested in seeing if they are resolved after each other or with Promise.all. Also, is this called when the request is done for the user or when the server closes? (I don't know enough, but maybe the server won't close if there's another ongoing request?)

I'm not too familiar with Cloudflare, but I'm definitely not thinking of queues in the sense that there is a worker process that handles jobs. I'm thinking staying within the request lifecycle.

@nandi95
Copy link
Author

nandi95 commented Sep 8, 2023

Also, would adding the getRequestFingerprint be of any use? Regardless of this discussion.

@nandi95
Copy link
Author

nandi95 commented Sep 8, 2023

Can the waitUntil be updated so it can take regular functions too? Maybe some people want to do some more heavy computation that is in sync, but the user doesn't have to wait for it.

@pi0 pi0 changed the title Running tasks after response Support differed functions with waitUntil Sep 8, 2023
@pi0 pi0 added enhancement New feature or request and removed question Further information is requested labels Sep 8, 2023
@pi0
Copy link
Member

pi0 commented Sep 8, 2023

Thanks for the feedbacks. The initial API was designed following up cloudflare workers. They expect you to give the util a promise that is already running in parallel to the request.

I think we can as well allow passing functions to waitUntil and internally call them in after response hook (in app.ts). This seems an ergonomic feature 👍🏼

@nandi95
Copy link
Author

nandi95 commented Sep 8, 2023

what are your thoughts on the getRequestFingerprint and the renaming of waitUntil? Will other providers be supported that aren't Cloudflare workers?

@pi0
Copy link
Member

pi0 commented Sep 8, 2023

Since we already introduced (platform agnostic) waitUntil, deprecating or introducing a second similar util might be confusing. Sometimes it is actually more useful to run tasks in parallel. But also makes sense to support deferred tasks (which could be deferred with other strategies)

You can also use onAfterResponse hook with h3 object syntax (https://unjs.io/blog/2023-08-15-h3-towards-the-edge-of-the-web#object-syntax-event-handlers) as well or make a similar util leveraging global afterResponse hook and plugins.

Re getRequestFingerprint, I think it could be a nice potential for h3 utils more. We can build it with uncrypto util to make sure remains platform agnostic (feel free to open an issue there 👍🏼 ).

@pi0 pi0 changed the title Support differed functions with waitUntil Support deferred functions with waitUntil Sep 8, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants