From 2b6554c707a4e53ec2b36388cbd5666d4d35d2cc Mon Sep 17 00:00:00 2001 From: Aral Roca Gomez Date: Thu, 12 Dec 2024 00:47:15 +0100 Subject: [PATCH] feat: add 'after' request context method (#677) --- .../components/request-context.md | 29 +++++++++++++++++++ packages/brisa/src/types/index.d.ts | 20 +++++++++++++ .../extend-request-context/index.test.ts | 27 +++++++++++++++++ .../src/utils/extend-request-context/index.ts | 6 ++++ .../render-to-readable-stream/index.test.tsx | 29 +++++++++++++++++++ .../utils/render-to-readable-stream/index.ts | 3 ++ 6 files changed, 114 insertions(+) diff --git a/docs/api-reference/components/request-context.md b/docs/api-reference/components/request-context.md index add1ad4b..30b153f4 100644 --- a/docs/api-reference/components/request-context.md +++ b/docs/api-reference/components/request-context.md @@ -37,6 +37,9 @@ export default function ServerComponent(props, requestContext: RequestContext) { // Add styles css, + + // Run tasks after the response is sent + after, } = requestContext; // ... Server component implementation ... } @@ -320,3 +323,29 @@ css` > We recommend using the `css` template literal for specific cases such as generating CSS animations based on dynamic JavaScript variables. For more details, refer to the [Template literal `css`](/building-your-application/components-details/web-components#template-literal-css) documentation. + +## `after` + +`after(cb: () => void): void` + +The `after` method allows you to schedule work to be executed after a response (or prerender) is finished. This is useful for tasks and other side effects that should not block the response, such as logging and analytics. + +It can be used everywhere when you have access to the `RequestContext` (Middleware, API routes, Server components, etc). + +Example: + +```tsx +import { type RequestContext } from "brisa"; + +export default function SomeComponent({}, { after }: RequestContext) { + after(() => { + console.log("The response is sent"); + }); + + return
Some content
; +} +``` + +> [!NOTE] +> +> **Good to know**: `after` is not a Dynamic API and calling it does not cause a route to become dynamic. If it's used within a static page, the callback will execute at build time. diff --git a/packages/brisa/src/types/index.d.ts b/packages/brisa/src/types/index.d.ts index ac7d6083..c586d6b3 100644 --- a/packages/brisa/src/types/index.d.ts +++ b/packages/brisa/src/types/index.d.ts @@ -353,6 +353,26 @@ export interface RequestContext extends Request { * - [How to use `css`](https://brisa.build/api-reference/components/request-context#css) */ css(strings: TemplateStringsArray, ...values: string[]): void; + + /** + * Description: + * + * The `after` method is used to execute a function after the response has been sent. + * + * Example: + * + * ```ts + * after(() => console.log('Hello World')); + * ``` + * + * This log will be executed after the response has been sent. + * + * Docs: + * + * - [How to use `after`](https://brisa.build/api-reference/components/request-context#after) + * + */ + after(fn: Effect): void | Promise; } type Effect = () => void | Promise; diff --git a/packages/brisa/src/utils/extend-request-context/index.test.ts b/packages/brisa/src/utils/extend-request-context/index.test.ts index 230b81b4..cfd65ffb 100644 --- a/packages/brisa/src/utils/extend-request-context/index.test.ts +++ b/packages/brisa/src/utils/extend-request-context/index.test.ts @@ -374,5 +374,32 @@ describe('brisa core', () => { }); expect(requestContext.initiator).toBe(Initiator.SERVER_ACTION); }); + + it('should add tasks to the request with "after"', () => { + const requestContext = extendRequestContext({ + originalRequest: new Request('https://example.com'), + }); + + const mockAfter = mock(() => {}); + requestContext.after(() => mockAfter()); + + expect((requestContext as any)._tasks).toHaveLength(1); + expect(mockAfter).not.toHaveBeenCalled(); + }); + + it('should keep tasks from one req to another one', () => { + const requestContext = extendRequestContext({ + originalRequest: new Request('https://example.com'), + }); + + requestContext.after(() => {}); + expect((requestContext as any)._tasks).toHaveLength(1); + + const requestContext2 = extendRequestContext({ + originalRequest: requestContext, + }); + + expect((requestContext as any)._tasks).toHaveLength(1); + }); }); }); diff --git a/packages/brisa/src/utils/extend-request-context/index.ts b/packages/brisa/src/utils/extend-request-context/index.ts index bb250574..529574a1 100644 --- a/packages/brisa/src/utils/extend-request-context/index.ts +++ b/packages/brisa/src/utils/extend-request-context/index.ts @@ -29,6 +29,12 @@ export default function extendRequestContext({ finalURL, id, }: ExtendRequestContext): RequestContext { + // After tasks + originalRequest._tasks ??= []; + originalRequest.after = (task) => { + originalRequest._tasks.push(task); + }; + // finalURL originalRequest.finalURL = finalURL ?? originalRequest.finalURL ?? originalRequest.url; diff --git a/packages/brisa/src/utils/render-to-readable-stream/index.test.tsx b/packages/brisa/src/utils/render-to-readable-stream/index.test.tsx index bd75a37e..73fb29d2 100644 --- a/packages/brisa/src/utils/render-to-readable-stream/index.test.tsx +++ b/packages/brisa/src/utils/render-to-readable-stream/index.test.tsx @@ -3796,6 +3796,35 @@ describe('utils', () => { ); }); + it('should resolve request._tasks after the response stream', async () => { + const request = extendRequestContext({ + originalRequest: extendRequestContext({ + originalRequest: new Request('http://test.com/en'), + }), + }); + + const mockAfter = mock(() => {}); + request.after(() => mockAfter()); + + const stream = renderToReadableStream( + + + + , + { request }, + ); + + const reader = stream.getReader(); + + while (true) { + const result = await reader.read(); + if (result.done) break; + expect(mockAfter).not.toHaveBeenCalled(); + } + + expect(mockAfter).toHaveBeenCalled(); + }); + it('should include window.r with the route without i18n', () => { const request = extendRequestContext({ originalRequest: extendRequestContext({ diff --git a/packages/brisa/src/utils/render-to-readable-stream/index.ts b/packages/brisa/src/utils/render-to-readable-stream/index.ts index 1f5f1ca2..d072bdec 100644 --- a/packages/brisa/src/utils/render-to-readable-stream/index.ts +++ b/packages/brisa/src/utils/render-to-readable-stream/index.ts @@ -110,6 +110,9 @@ export default function renderToReadableStream( controller.close(); + // Resolve tasks from "after" request method + await Promise.all((req as any)._tasks.map((task: () => void) => task())); + if ( isPage && !IS_PRODUCTION &&