Skip to content

Commit

Permalink
feat: experimental composition api via useEvent() ans async context…
Browse files Browse the repository at this point in the history
… support (#1546)
  • Loading branch information
pi0 authored Aug 7, 2023
1 parent 346a61a commit 6669fb2
Show file tree
Hide file tree
Showing 13 changed files with 166 additions and 9 deletions.
60 changes: 58 additions & 2 deletions docs/content/1.guide/6.utils.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ Nitro helps you to stay organized allowing you to take advantage of the [`auto-i

Every export in the `utils` directory and its subdirectories will become available globally in your application.

## Example
---

Create a `utils/sum.ts` file where a function `useSum` is exported:
**Example:** Create a `utils/sum.ts` file where a function `useSum` is exported:

```ts [utils/sum.ts]
export function useSum(a: number, b: number) { return a + b }
Expand All @@ -25,3 +25,59 @@ export default defineEventHandler(() => {
return { sum }
})
```

---

## Experimental Composition API

Nitro (2.6+) enables a new server development experience in order to split application logic into smaller "composable" utilities that are fully decoupled from each other and can directly assess to a shared context (request event) without needing it to be passed along. This pattern is inspired from [Vue Composition API](https://vuejs.org/guide/extras/composition-api-faq.html#why-composition-api) and powered by [unjs/unctx](https://github.com/unjs/unctx).

This feature is currently supported for Node.js and Bun runtimes and also coming soon to other presets that support [`AsyncLocalStorage`](https://nodejs.org/api/async_context.html#class-asynclocalstorage) interface.

In order to enable composition API, you have to enable `asyncContext` flag:

::code-group
```ts [nitro.config.ts]
export default defineNitroConfig({
experimental: {
asyncContext: true
}
});
```
```ts [nuxt.config.ts]
export default defineNuxtConfig({
nitro: {
experimental: {
asyncContext: true
}
}
})
```
::

After enabling this flag, you can use `useEvent()` (auto imported) in any utility or composable to access the request event without manually passing it along:

::code-group
```ts [with async context]
// routes/index.ts
export default defineEventHandler(async () => {
const user = await useAuth()
})

// utils/auth.ts
export function useAuth() {
return useSession(useEvent())
}
```
```ts [without async context]
// routes/index.ts
export default defineEventHandler(async (event) => {
const user = await useAuth(event)
})

// utils/auth.ts
export function useAuth(event) {
return useSession(event)
}
```
::
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@
"std-env": "^3.3.3",
"ufo": "^1.2.0",
"uncrypto": "^0.1.3",
"unenv": "^1.6.1",
"unctx": "^2.3.1",
"unenv": "^1.6.2",
"unimport": "^3.1.3",
"unstorage": "^1.8.0"
},
Expand Down
28 changes: 23 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/imports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const nitroImports: Preset[] = [
"defineRenderHandler",
"getRouteRules",
"useAppConfig",
"useEvent",
],
},
];
2 changes: 2 additions & 0 deletions src/rollup/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,8 @@ export const getRollupConfig = (nitro: Nitro): RollupConfig => {
// @ts-expect-error
"versions.nitro": version,
"versions?.nitro": version,
// Internal
_asyncContext: nitro.options.experimental.asyncContext,
};

// Universal import.meta
Expand Down
10 changes: 10 additions & 0 deletions src/runtime/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { useRuntimeConfig } from "./config";
import { cachedEventHandler } from "./cache";
import { normalizeFetchResponse } from "./utils";
import { createRouteRulesHandler, getRouteRulesForPath } from "./route-rules";
import { NitroAsyncContext, nitroAsyncContext } from "./context";
import type { $Fetch, NitroFetchRequest } from "nitropack";
import { plugins } from "#internal/nitro/virtual/plugins";
import errorHandler from "#internal/nitro/virtual/error-handler";
Expand Down Expand Up @@ -172,6 +173,15 @@ function createNitroApp(): NitroApp {

h3App.use(config.app.baseURL as string, router.handler);

// Experimental async context support
if (import.meta._asyncContext) {
const _handler = h3App.handler;
h3App.handler = (event) => {
const ctx: NitroAsyncContext = { event };
return nitroAsyncContext.callAsync(ctx, () => _handler(event));
};
}

const app: NitroApp = {
hooks,
h3App,
Expand Down
34 changes: 34 additions & 0 deletions src/runtime/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { AsyncLocalStorage } from "node:async_hooks";
import { H3Event, createError } from "h3";
import { getContext } from "unctx";

export interface NitroAsyncContext {
event: H3Event;
}

export const nitroAsyncContext = getContext<NitroAsyncContext>("nitro-app", {
asyncContext: import.meta._asyncContext,
AsyncLocalStorage: import.meta._asyncContext ? AsyncLocalStorage : undefined,
});

/**
*
* Access to the current Nitro request event.
*
* @experimental
* - Requires `experimental.asyncContext: true` config to work.
* - Works in Node.js and limited runtimes only
*
*/
export function useEvent(): H3Event {
try {
return nitroAsyncContext.use().event;
} catch {
const hint = import.meta._asyncContext
? "Note: This is an experimental feature and might be broken on non-Node.js environments."
: "Enable the experimental flag using `experimental.asyncContext: true`.";
throw createError({
message: `Nitro request context is not available. ${hint}`,
});
}
}
1 change: 1 addition & 0 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./plugin";
export * from "./renderer";
export { getRouteRules } from "./route-rules";
export { useStorage } from "./storage";
export { useEvent } from "./context";
1 change: 1 addition & 0 deletions src/types/global.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NitroOptions } from "./nitro";

export interface NitroStaticBuildFlags {
_asyncContext?: boolean;
dev?: boolean;
client?: boolean;
nitro?: boolean;
Expand Down
4 changes: 4 additions & 0 deletions src/types/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,10 @@ export interface NitroOptions extends PresetOptions {
* See https://github.com/microsoft/TypeScript/pull/51669
*/
typescriptBundlerResolution?: boolean;
/**
* Enable native async context support for useEvent()
*/
asyncContext?: boolean;
};
future: {
nativeSWR: boolean;
Expand Down
1 change: 1 addition & 0 deletions test/fixture/nitro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,6 @@ export default defineNitroConfig({
},
experimental: {
openAPI: true,
asyncContext: true,
},
});
12 changes: 12 additions & 0 deletions test/fixture/routes/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default defineEventHandler(async () => {
await Promise.resolve(setTimeout(() => {}, 10));
return await useTest();
});

function useTest() {
return {
context: {
path: useEvent().path,
},
};
}
18 changes: 17 additions & 1 deletion test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,12 @@ export function testNitro(
// TODO: Node presets do not split cookies
// https://github.com/unjs/nitro/issues/1462
// (vercel and deno-server uses node only for tests only)
const notSplitingPresets = ["node", "nitro-dev", "vercel", nodeVersion < 18 && "deno-server"].filter(Boolean);
const notSplitingPresets = [
"node",
"nitro-dev",
"vercel",
nodeVersion < 18 && "deno-server",
].filter(Boolean);
if (notSplitingPresets.includes(ctx.preset)) {
expectedCookies =
nodeVersion < 18
Expand Down Expand Up @@ -523,4 +528,15 @@ export function testNitro(
expect(allErrorMessages).to.includes("Service Unavailable");
});
});

describe("async context", () => {
it.skipIf(!ctx.nitro.options.node)("works", async () => {
const { data } = await callHandler({ url: "/context?foo" });
expect(data).toMatchObject({
context: {
path: "/context?foo",
},
});
});
});
}

0 comments on commit 6669fb2

Please sign in to comment.