forked from vercel/next.js
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(edge): adds AsyncLocalStorage support to the edge function sandb…
…ox (vercel#41622) ## 📖 Feature Adds `AsyncLocalStorage` as a global variable to any edge function (middleware, Edge API routes). Falls back to Node.js' implementation. ## 🧪 How to test 1. `pnpm build` 2. `pnpm testheadless --testPathPattern async-local`
- Loading branch information
Showing
2 changed files
with
129 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/* eslint-disable jest/valid-expect-in-promise */ | ||
import { createNext } from 'e2e-utils' | ||
import { NextInstance } from 'test/lib/next-modes/base' | ||
import { fetchViaHTTP } from 'next-test-utils' | ||
|
||
describe('edge api can use async local storage', () => { | ||
let next: NextInstance | ||
|
||
const cases = [ | ||
{ | ||
title: 'a single instance', | ||
code: ` | ||
export const config = { runtime: 'experimental-edge' } | ||
const storage = new AsyncLocalStorage() | ||
export default async function handler(request) { | ||
const id = request.headers.get('req-id') | ||
return storage.run({ id }, async () => { | ||
await getSomeData() | ||
return Response.json(storage.getStore()) | ||
}) | ||
} | ||
async function getSomeData() { | ||
try { | ||
const response = await fetch('https://example.vercel.sh') | ||
await response.text() | ||
} finally { | ||
return true | ||
} | ||
} | ||
`, | ||
expectResponse: (response, id) => | ||
expect(response).toMatchObject({ status: 200, json: { id } }), | ||
}, | ||
{ | ||
title: 'multiple instances', | ||
code: ` | ||
export const config = { runtime: 'experimental-edge' } | ||
const topStorage = new AsyncLocalStorage() | ||
export default async function handler(request) { | ||
const id = request.headers.get('req-id') | ||
return topStorage.run({ id }, async () => { | ||
const nested = await getSomeData(id) | ||
return Response.json({ ...nested, ...topStorage.getStore() }) | ||
}) | ||
} | ||
async function getSomeData(id) { | ||
const nestedStorage = new AsyncLocalStorage() | ||
return nestedStorage.run('nested-' + id, async () => { | ||
try { | ||
const response = await fetch('https://example.vercel.sh') | ||
await response.text() | ||
} finally { | ||
return { nestedId: nestedStorage.getStore() } | ||
} | ||
}) | ||
} | ||
`, | ||
expectResponse: (response, id) => | ||
expect(response).toMatchObject({ | ||
status: 200, | ||
json: { id: id, nestedId: `nested-${id}` }, | ||
}), | ||
}, | ||
] | ||
|
||
afterEach(() => next.destroy()) | ||
|
||
it.each(cases)( | ||
'cans use $title per request', | ||
async ({ code, expectResponse }) => { | ||
next = await createNext({ | ||
files: { | ||
'pages/index.js': ` | ||
export default function () { return <div>Hello, world!</div> } | ||
`, | ||
'pages/api/async.js': code, | ||
}, | ||
}) | ||
const ids = Array.from({ length: 100 }, (_, i) => `req-${i}`) | ||
|
||
const responses = await Promise.all( | ||
ids.map((id) => | ||
fetchViaHTTP( | ||
next.url, | ||
'/api/async', | ||
{}, | ||
{ headers: { 'req-id': id } } | ||
).then((response) => | ||
response.headers.get('content-type')?.startsWith('application/json') | ||
? response.json().then((json) => ({ | ||
status: response.status, | ||
json, | ||
text: null, | ||
})) | ||
: response.text().then((text) => ({ | ||
status: response.status, | ||
json: null, | ||
text, | ||
})) | ||
) | ||
) | ||
) | ||
const rankById = new Map(ids.map((id, rank) => [id, rank])) | ||
|
||
const errors: Error[] = [] | ||
for (const [rank, response] of responses.entries()) { | ||
try { | ||
expectResponse(response, ids[rank]) | ||
} catch (error) { | ||
const received = response.json?.id | ||
console.log( | ||
`response #${rank} has id from request #${rankById.get(received)}` | ||
) | ||
errors.push(error as Error) | ||
} | ||
} | ||
if (errors.length) { | ||
throw errors[0] | ||
} | ||
} | ||
) | ||
}) |