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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(edge): adds AsyncLocalStorage support to the edge function sandbox #41622

Merged
merged 2 commits into from Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/next/server/web/sandbox/context.ts
@@ -1,3 +1,4 @@
import { AsyncLocalStorage } from 'async_hooks'
import type { AssetBinding } from '../../../build/webpack/loaders/get-module-build-info'
import {
decorateServerError,
Expand Down Expand Up @@ -286,6 +287,8 @@ Learn More: https://nextjs.org/docs/messages/edge-dynamic-code-evaluation`),

Object.assign(context, wasm)

context.AsyncLocalStorage = AsyncLocalStorage

return context
},
})
Expand Down
106 changes: 106 additions & 0 deletions test/e2e/edge-async-local-storage/index.test.ts
@@ -0,0 +1,106 @@
/* 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() {
const response = await fetch('https://example.vercel.sh')
return response.text()
}
`,
expectResponse: (response, id) => expect(response).toMatchObject({ 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 () => {
const response = await fetch('https://example.vercel.sh')
await response.text()
return { nestedId: nestedStorage.getStore() }
})
}
`,
expectResponse: (response, id) =>
expect(response).toMatchObject({
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.json())
)
)
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]
}
}
)
})