diff --git a/.changeset/unlucky-masks-cheat.md b/.changeset/unlucky-masks-cheat.md new file mode 100644 index 00000000..4619f761 --- /dev/null +++ b/.changeset/unlucky-masks-cheat.md @@ -0,0 +1,7 @@ +--- +"@edge-runtime/node-utils": minor +"@edge-runtime/primitives": minor +"@edge-runtime/vm": minor +--- + +Expose `context.waitUntil` for Node.js diff --git a/packages/node-utils/src/node-to-edge/fetch-event.ts b/packages/node-utils/src/node-to-edge/fetch-event.ts index 62fbb215..acc57351 100644 --- a/packages/node-utils/src/node-to-edge/fetch-event.ts +++ b/packages/node-utils/src/node-to-edge/fetch-event.ts @@ -3,14 +3,6 @@ import { BuildDependencies } from '../types' export function buildToFetchEvent(dependencies: BuildDependencies) { return function toFetchEvent(request: Request) { - const event = new dependencies.FetchEvent(request) - Object.defineProperty(event, 'waitUntil', { - configurable: false, - enumerable: true, - get: () => { - throw new Error('waitUntil is not supported yet.') - }, - }) - return event + return new dependencies.FetchEvent(request) } } diff --git a/packages/node-utils/test/edge-to-node/handler.test.ts b/packages/node-utils/test/edge-to-node/handler.test.ts index 1377b888..62a830e6 100644 --- a/packages/node-utils/test/edge-to-node/handler.test.ts +++ b/packages/node-utils/test/edge-to-node/handler.test.ts @@ -4,12 +4,21 @@ import { runTestServer } from '../test-utils/run-test-server' import { serializeResponse } from '../test-utils/serialize-response' import * as Edge from '@edge-runtime/primitives' +function createDeferred() { + let resolve, reject + const promise = new Promise((_resolve, _reject) => { + resolve = _resolve + reject = _reject + }) + return { promise, resolve, reject } +} + const transformToNode = buildToNodeHandler( { Headers: Edge.Headers, ReadableStream: Edge.ReadableStream, Request: Edge.Request, - Uint8Array: Uint8Array, + Uint8Array, FetchEvent: Edge.FetchEvent, }, { defaultOrigin: 'http://example.com' }, @@ -231,18 +240,30 @@ it('consumes incoming headers', async () => { }) }) -it('fails when using waitUntil()', async () => { +it('interacts with waitUntil', async () => { + let emitted = false + const deferred = createDeferred() server = await runTestServer({ - handler: transformToNode((req, evt) => { - evt.waitUntil(Promise.resolve()) - return new Edge.Response('ok') + handler: transformToNode((_, { waitUntil }) => { + waitUntil( + new Promise((resolve) => + setTimeout(() => { + emitted = true + // @ts-expect-error + deferred.resolve(Date.now()) + resolve() + }, 1000), + ), + ) + return new Edge.Response('Hello world') }), }) + const start = Date.now() const response = await server.fetch('/') - expect(await serializeResponse(response)).toMatchObject({ - status: 500, - statusText: 'Internal Server Error', - text: 'Error: waitUntil is not supported yet.', - }) + const end = (await deferred.promise) as number + expect(emitted).toBe(true) + expect(end - start).toBeGreaterThanOrEqual(1000) + expect(response.status).toBe(200) + expect(await response.text()).toBe('Hello world') }) diff --git a/packages/node-utils/test/node-to-edge/fetch-event.test.ts b/packages/node-utils/test/node-to-edge/fetch-event.test.ts index 8a702620..ac2f61d0 100644 --- a/packages/node-utils/test/node-to-edge/fetch-event.test.ts +++ b/packages/node-utils/test/node-to-edge/fetch-event.test.ts @@ -16,10 +16,12 @@ it('returns a fetch event with a request', () => { expect(event.request).toBe(request) }) -it('throws when accessing waitUntil', () => { +it('interacts with waitUntil', async () => { const request = new EdgeRuntime.Request('https://vercel.com') const event = toFetchEvent(request) - expect(() => event.waitUntil(Promise.resolve())).toThrow( - 'waitUntil is not supported yet.', - ) + let duration = Date.now() + event.waitUntil(new Promise((resolve) => setTimeout(resolve, 1000))) + await Promise.all(event.awaiting) + duration = Date.now() - duration + expect(duration).toBeGreaterThanOrEqual(1000) }) diff --git a/packages/primitives/src/primitives/events.js b/packages/primitives/src/primitives/events.js index 9dc13eef..20169115 100644 --- a/packages/primitives/src/primitives/events.js +++ b/packages/primitives/src/primitives/events.js @@ -6,11 +6,11 @@ export class FetchEvent extends Event { this.awaiting = new Set() } - respondWith(response) { + respondWith = (response) => { this.response = response } - waitUntil(promise) { + waitUntil = (promise) => { this.awaiting.add(promise) promise.finally(() => this.awaiting.delete(promise)) } diff --git a/packages/vm/tests/edge-runtime.test.ts b/packages/vm/tests/edge-runtime.test.ts index 8141fda3..c73918f7 100644 --- a/packages/vm/tests/edge-runtime.test.ts +++ b/packages/vm/tests/edge-runtime.test.ts @@ -475,6 +475,25 @@ describe('Event handlers', () => { expect(promises).toEqual([50, 500]) }) + it('allows to destructure `waitUntil` and `respondWith', async () => { + const runtime = new EdgeVM({ + initialCode: ` + const delay = ms => new Promise(resolve => { + setTimeout(() => resolve(ms), ms) + }); + + addEventListener('fetch', ({ waitUntil, respondWith }) => { + waitUntil(Promise.resolve(delay(50))) + waitUntil(Promise.resolve(delay(500))) + respondWith(new Response()) + }); + `, + }) + + const res = await runtime.dispatchFetch('https://edge-ping.vercel.app') + expect(res.status).toBe(200) + }) + it('allows to add a `fetch` event handler that responds without leaking server headers on a 200', async () => { const runtime = new EdgeVM()