From ad6cda43000a2f66596ded0f76e3cfc5160acb78 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Tue, 10 Jan 2023 15:54:48 -0800 Subject: [PATCH 1/3] Implement potentiallyLongRunningResponse helper in `@vercel/edge` This helper produces an immediate response, which writes the data once the given promise is resolved. --- packages/edge/docs/README.md | 38 ++++++++++++++++++++++++++ packages/edge/src/response.ts | 38 ++++++++++++++++++++++++++ packages/edge/test/response.test.ts | 42 ++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 1 deletion(-) diff --git a/packages/edge/docs/README.md b/packages/edge/docs/README.md index 20b40766d2..a2407678f1 100644 --- a/packages/edge/docs/README.md +++ b/packages/edge/docs/README.md @@ -24,6 +24,7 @@ - [ipAddress](README.md#ipaddress) - [json](README.md#json) - [next](README.md#next) +- [potentiallyLongRunningResponse](README.md#potentiallylongrunningresponse) - [rewrite](README.md#rewrite) ## Variables @@ -261,6 +262,43 @@ export default function middleware(_req: Request) { --- +### potentiallyLongRunningResponse + +▸ **potentiallyLongRunningResponse**(`dataPromise`, `init?`): `Response` + +Builds a response for returning data based on promise that take many seconds to resolve. +The response is returned immediately, but data is only written to it when the promise resolves. + +**`Example`** + +```ts +import { potentiallyLongRunningResponse } from '@vercel/edge'; + +export default () => { + const slowPromise = new Promise(resolve => + setTimeout(() => resolve('Done'), 20000) + ); + return potentiallyLongRunningResponse(slowPromise); +}; +``` + +#### Parameters + +| Name | Type | Description | +| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------- | +| `dataPromise` | [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<`string` \| [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)\> | Promise for data. Supported types of data are string and Uint Uint8Array | +| `init?` | `ResponseInit` | optional custom response status, statusText and headers | + +#### Returns + +`Response` + +#### Defined in + +[src/response.ts:41](https://github.com/vercel/vercel/blob/main/packages/edge/src/response.ts#L41) + +--- + ### rewrite ▸ **rewrite**(`destination`, `init?`): `Response` diff --git a/packages/edge/src/response.ts b/packages/edge/src/response.ts index 57c64487a5..62dd343988 100644 --- a/packages/edge/src/response.ts +++ b/packages/edge/src/response.ts @@ -20,3 +20,41 @@ export function json(data: any, init?: ResponseInit): Response { // @ts-expect-error This is not in lib/dom right now, and we can't augment it. return Response.json(data, init); } + +/** + * Builds a response for returning data based on promise that take many seconds to resolve. + * The response is returned immediately, but data is only written to it when the promise resolves. + * + * @param dataPromise Promise for data. Supported types of data are string and Uint Uint8Array + * @param init optional custom response status, statusText and headers + * + * @example + * ```ts + * import { potentiallyLongRunningResponse } from '@vercel/edge'; + * + * export default () => { + * const slowPromise = new Promise((resolve) => setTimeout(() => resolve("Done"), 20000)); + * return potentiallyLongRunningResponse(slowPromise); + * }; + * ``` + */ +export function potentiallyLongRunningResponse( + dataPromise: Promise, + init?: ResponseInit +): Response { + return new Response( + new ReadableStream({ + start(controller) { + dataPromise.then((data: string | Uint8Array) => { + if (typeof data === 'string') { + controller.enqueue(new TextEncoder().encode(data)); + } else { + controller.enqueue(data); + } + controller.close(); + }); + }, + }), + init + ); +} diff --git a/packages/edge/test/response.test.ts b/packages/edge/test/response.test.ts index 1b361d8754..f40520dd2f 100644 --- a/packages/edge/test/response.test.ts +++ b/packages/edge/test/response.test.ts @@ -2,7 +2,7 @@ * @jest-environment @edge-runtime/jest-environment */ -import { json } from '../src/response'; +import { json, potentiallyLongRunningResponse } from '../src/response'; describe('json', () => { it('returns a response with JSON content', async () => { @@ -32,3 +32,43 @@ describe('json', () => { expect(await response.json()).toEqual(content); }); }); + +describe('potentiallyLongRunningResponse', () => { + it('returns a response with immediate data', async () => { + const response = potentiallyLongRunningResponse( + new Promise(resolve => resolve('test')) + ); + expect(await response.text()).toBe('test'); + }); + + it('returns a response after a timeout', async () => { + const slowPromise: Promise = new Promise(resolve => + setTimeout(() => resolve('after timeout'), 1000) + ); + const response = potentiallyLongRunningResponse(slowPromise); + expect(await response.text()).toBe('after timeout'); + }); + + it('returns a response with custom init', async () => { + const response = potentiallyLongRunningResponse( + new Promise(resolve => resolve('test')), + { + status: 400, + headers: { + 'content-type': 'text/custom', + }, + } + ); + expect(await response.text()).toBe('test'); + expect(response.status).toBe(400); + expect(response.headers.get('content-type')).toBe('text/custom'); + }); + + it('returns a response with Uint8Array data', async () => { + const data = new TextEncoder().encode('data'); + const response = potentiallyLongRunningResponse( + new Promise(resolve => resolve(data)) + ); + expect(await response.text()).toBe('data'); + }); +}); From d307a866ba8b6c06a595edf811e9be9d422ab612 Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 11 Jan 2023 11:13:51 -0800 Subject: [PATCH 2/3] Update packages/edge/src/response.ts Co-authored-by: Gal Schlezinger --- packages/edge/src/response.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edge/src/response.ts b/packages/edge/src/response.ts index 62dd343988..a9aaf64ab5 100644 --- a/packages/edge/src/response.ts +++ b/packages/edge/src/response.ts @@ -25,7 +25,7 @@ export function json(data: any, init?: ResponseInit): Response { * Builds a response for returning data based on promise that take many seconds to resolve. * The response is returned immediately, but data is only written to it when the promise resolves. * - * @param dataPromise Promise for data. Supported types of data are string and Uint Uint8Array + * @param dataPromise Promise for data to be sent as the response body * @param init optional custom response status, statusText and headers * * @example From 0c1d0d3e86a006cf0754cc95b285c4c7457b9b1f Mon Sep 17 00:00:00 2001 From: Malte Ubl Date: Wed, 11 Jan 2023 11:20:02 -0800 Subject: [PATCH 3/3] Add error handling --- packages/edge/docs/README.md | 10 +++++----- packages/edge/src/response.ts | 28 +++++++++++++++++++--------- packages/edge/test/response.test.ts | 7 +++++++ 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/edge/docs/README.md b/packages/edge/docs/README.md index a2407678f1..666a01dc09 100644 --- a/packages/edge/docs/README.md +++ b/packages/edge/docs/README.md @@ -284,10 +284,10 @@ export default () => { #### Parameters -| Name | Type | Description | -| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------------- | -| `dataPromise` | [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<`string` \| [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)\> | Promise for data. Supported types of data are string and Uint Uint8Array | -| `init?` | `ResponseInit` | optional custom response status, statusText and headers | +| Name | Type | Description | +| :------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `dataPromise` | [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise)<`string` \| [`Uint8Array`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array)\> | Promise for data to be sent as the response body. Note, that if this promise is rejected, then a plain text "ERROR" is returned to the cliet. Catch errors on the promise yourself to add custom error handling. | +| `init?` | `ResponseInit` | optional custom response status, statusText and headers | #### Returns @@ -295,7 +295,7 @@ export default () => { #### Defined in -[src/response.ts:41](https://github.com/vercel/vercel/blob/main/packages/edge/src/response.ts#L41) +[src/response.ts:43](https://github.com/vercel/vercel/blob/main/packages/edge/src/response.ts#L43) --- diff --git a/packages/edge/src/response.ts b/packages/edge/src/response.ts index a9aaf64ab5..f57462fe1c 100644 --- a/packages/edge/src/response.ts +++ b/packages/edge/src/response.ts @@ -25,7 +25,9 @@ export function json(data: any, init?: ResponseInit): Response { * Builds a response for returning data based on promise that take many seconds to resolve. * The response is returned immediately, but data is only written to it when the promise resolves. * - * @param dataPromise Promise for data to be sent as the response body + * @param dataPromise Promise for data to be sent as the response body. Note, that if this promise is + * rejected, then a plain text "ERROR" is returned to the cliet. Catch errors on the promise yourself + * to add custom error handling. * @param init optional custom response status, statusText and headers * * @example @@ -45,14 +47,22 @@ export function potentiallyLongRunningResponse( return new Response( new ReadableStream({ start(controller) { - dataPromise.then((data: string | Uint8Array) => { - if (typeof data === 'string') { - controller.enqueue(new TextEncoder().encode(data)); - } else { - controller.enqueue(data); - } - controller.close(); - }); + dataPromise + .then((data: string | Uint8Array) => { + if (typeof data === 'string') { + controller.enqueue(new TextEncoder().encode(data)); + } else { + controller.enqueue(data); + } + controller.close(); + }) + .catch(error => { + console.log( + `Error in 'potentiallyLongRunningResponse' dataPromise: ${error}` + ); + controller.enqueue(new TextEncoder().encode('ERROR')); + controller.close(); + }); }, }), init diff --git a/packages/edge/test/response.test.ts b/packages/edge/test/response.test.ts index f40520dd2f..d73a8ff781 100644 --- a/packages/edge/test/response.test.ts +++ b/packages/edge/test/response.test.ts @@ -71,4 +71,11 @@ describe('potentiallyLongRunningResponse', () => { ); expect(await response.text()).toBe('data'); }); + + it('returns ERROR on rejected promise', async () => { + const response = potentiallyLongRunningResponse( + new Promise((_, reject) => reject(new Error('test'))) + ); + expect(await response.text()).toBe('ERROR'); + }); });