-
-
Notifications
You must be signed in to change notification settings - Fork 615
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: next()
without await
#1685
base: main
Are you sure you want to change the base?
Conversation
Hi @usualoma ! What do you think about this? Can we really use it without |
Hi @yusukebe! Hmmm, I'm sure there are apps that are synchronous and have no problem working without The following application will result in an Internal Server Error. import { createMiddleware } from './src/helper/factory'
import { Hono } from './src/hono'
const app = new Hono()
app.get(
'/asyncContent',
createMiddleware((c, next) => {
const foo = 'foo'
next()
}),
async (c) => {
const data = await (await fetch('https://ramen-api.dev/shops/takasagoya')).json()
return c.json(data)
}
)
export default app Or, as shown below, even if the outer middleware uses await, if middleware that does not use await is inserted in between, it will not work properly. import { createMiddleware } from './src/helper/factory'
import { Hono } from './src/hono'
const app = new Hono()
app.get(
'/asyncContent',
createMiddleware(async (c, next) => {
await next()
c.res = new Response('asyncContent ' + await c.res.text()) // rewrite response
}),
createMiddleware((c, next) => {
next()
}),
async (c) => {
const data = await (await fetch('https://ramen-api.dev/shops/takasagoya')).json()
return c.json(data)
}
)
export default app |
Hmm, you're right. Any ideas... I want to resolve this issue of increasing overhead when middleware is added. I think we can change the API just for this, and we could release it as v4. |
This is just an idea. I don't know how to implement it yet, but we propose syntax like this, though app.get(
'/asyncContent',
// POC
// You don't have to `await next()`
createMiddleware((c, next) => {
console.log('mw - before handler')
next(() => {
console.log('mw - after handler')
})
}),
async (c) => {
console.log('handler')
const data = await (await fetch('https://ramen-api.dev/shops/takasagoya')).json()
return c.json(data)
}
) |
If we could just avoid However, if we want to do something with the middleware after the next() operation, the middleware must return a Promise anyway, or else the dispatch will be over before the internal async is resolved. app.get(
'/asyncContent',
createMiddleware((c, next) => {
return next().then(() => {
c.res.text().then((text) => {
c.res = new Response('asyncContent ' + text) // rewrite response
})
})
}),
async (c) => {
const data = await (await fetch('https://ramen-api.dev/shops/takasagoya')).json()
return c.json(data)
}
) If you really want to reduce With this PR, we can add middleware that is "synchronous and does not call next()" while maintaining high performance. import { createMiddleware } from './src/helper/factory'
import { Hono } from './src/hono'
const app = new Hono()
app.get(
'/asyncContent/:name',
...Array.from(Array(10)).map((_, i) =>
createMiddleware('before', (c) => {
c.res.headers.set('overwrite-by-handler', 'by middleware')
c.res.headers.set(`Before-${i}`, 'Hono')
})
),
...Array.from(Array(10)).map((_, i) =>
createMiddleware('after', (c) => {
c.res.headers.set('not-overwrite-by-handler', 'by middleware')
c.res.headers.set(`After-${i}`, 'Hono')
})
),
async (c) => {
c.res.headers.set('overwrite-by-handler', 'by handler')
c.res.headers.set('not-overwrite-by-handler', 'by handler')
const data = await (await fetch(`https://ramen-api.dev/shops/${c.req.param('name')}`)).json()
return c.json(data)
}
)
export default app
|
I think #1689 is an interesting idea, but I am not sure that it is optimized enough to be meaningful in a production environment beyond producing good results in benchmark scripts. |
Hi @usualoma, Thanks! That's interesting. I'm also struggling to find a good solution. Let's take some time to work on it. |
How do you feel about this API, although it has not been implemented yet? const mwWithAwait = createMiddleware(async (c, next) => {
before()
await next()
after()
})
const mwWithoutAwait = createMiddleware((c, next) => {
before()
return next(after())
}) |
Yes, I think such a change is possible. diff --git a/src/compose.ts b/src/compose.ts
index 56a755b..bf1491f 100644
--- a/src/compose.ts
+++ b/src/compose.ts
@@ -42,9 +42,13 @@ export const compose = <C extends ComposeContext, E extends Env = Env>(
}
} else {
try {
- res = handler(context, () => {
+ res = handler(context, (after?: () => void | Promise<void>) => {
const dispatchRes = dispatch(i + 1)
- return dispatchRes instanceof Promise ? dispatchRes : Promise.resolve(dispatchRes)
+ return !after
+ ? dispatchRes
+ : dispatchRes instanceof Promise
+ ? dispatchRes.then(after)
+ : after()
})
} catch (err) {
if (err instanceof Error && context instanceof Context && onError) {
diff --git a/src/types.ts b/src/types.ts
index 0c031ac..7bec387 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -20,7 +20,7 @@ export type Env = {
Variables?: Variables
}
-export type Next = () => void | Promise<void>
+export type Next = (after?: () => void | Promise<void>) => void | Promise<void>
export type Input = {
in?: Partial<ValidationTargets> At this time, the following apps will "/mwWithoutAwait" faster import { createMiddleware } from './src/helper/factory'
import { Hono } from './src/hono'
const app = new Hono()
const mwWithAwait = createMiddleware(async (c, next) => {
const foo = 'foo'
await next()
const bar = 'bar'
})
const mwWithoutAwait = createMiddleware((c, next) => {
const foo = 'foo'
return next(() => {
const bar = 'bar'
})
})
app.get('/', (c) => c.text('hi'))
app.get('/mwWithAwait', mwWithAwait, mwWithAwait, mwWithAwait, mwWithAwait, mwWithAwait, (c) => c.text('hi'))
app.get('/mwWithoutAwait', mwWithoutAwait, mwWithoutAwait, mwWithoutAwait, mwWithoutAwait, mwWithoutAwait, (c) =>
c.text('hi')
)
export default app
However, if a Promise is returned from the handler, the difference is much smaller, although '/mwWithoutAwait is faster. import { createMiddleware } from './src/helper/factory'
import { Hono } from './src/hono'
const app = new Hono()
const mwWithAwait = createMiddleware(async (c, next) => {
const foo = 'foo'
await next()
const bar = 'bar'
})
const mwWithoutAwait = createMiddleware((c, next) => {
const foo = 'foo'
return next(() => {
const bar = 'bar'
})
})
app.get('/', (c) => c.text('hi'))
app.get('/mwWithAwait', mwWithAwait, mwWithAwait, mwWithAwait, mwWithAwait, mwWithAwait, (c) => c.text('hi'))
app.get('/mwWithoutAwait', mwWithoutAwait, mwWithoutAwait, mwWithoutAwait, mwWithoutAwait, mwWithoutAwait, (c) =>
Promise.resolve(c.text('hi'))
)
export default app
I believe this change will optimize performance only if all of the handlers and middleware are processed synchronously. In a production environment, most handlers will work asynchronously, so I think the optimization will be applied only in a few cases in a production environment. I think there will be a significant difference on the benchmark script. It would also be good if the official middleware is rewritten to support this. hono/src/middleware/basic-auth/index.ts Lines 46 to 50 in 864fae6
On the other hand, I think it is better to continue to encourage Hono users to write Also, with the above change, the following code will not produce a type error (the current main branch will produce an error) I think the inability to produce type errors is a major problem, so I think we will have to devise a solution (a rather difficult problem). const middlewareThatCausesProblemsInAsyncResult = createMiddleware((c, next) => {
const foo = 'foo'
next()
const bar = 'bar'
}) |
Thanks!
I agree with this too. I've also refactored diff --git a/src/compose.ts b/src/compose.ts
index 56a755b..b375dcd 100644
--- a/src/compose.ts
+++ b/src/compose.ts
@@ -17,7 +17,7 @@ export const compose = <C extends ComposeContext, E extends Env = Env>(
let index = -1
return dispatch(0)
- function dispatch(i: number): C | Promise<C> {
+ async function dispatch(i: number): Promise<C> {
if (i <= index) {
throw new Error('next() called multiple times')
}
@@ -42,9 +42,8 @@ export const compose = <C extends ComposeContext, E extends Env = Env>(
}
} else {
try {
- res = handler(context, () => {
- const dispatchRes = dispatch(i + 1)
- return dispatchRes instanceof Promise ? dispatchRes : Promise.resolve(dispatchRes)
+ res = await handler(context, () => {
+ return dispatch(i + 1)
})
} catch (err) {
if (err instanceof Error and context instanceof Context and onError) {
@@ -57,34 +56,13 @@ export const compose = <C extends ComposeContext, E extends Env = Env>(
}
}
- if (!(res instanceof Promise)) {
- if (res !== undefined and 'response' in res) {
- res = res['response']
- }
- if (res and (context.finalized === false or isError)) {
- context.res = res
- }
- return context
- } else {
- return res
- .then((res) => {
- if (res !== undefined and 'response' in res) {
- res = res['response']
- }
- if (res and context.finalized === false) {
- context.res = res
- }
- return context
- })
- .catch(async (err) => {
- if (err instanceof Error and context instanceof Context and onError) {
- context.error = err
- context.res = await onError(err, context)
- return context
- }
- throw err
- })
+ if (res !== undefined and 'response' in res) {
+ res = res['response']
+ }
+ if (res and (context.finalized === false or isError)) {
+ context.res = res
}
+ return context
}
}
} We need to benchmark again to be sure, but if this improves performance, it would be a good change. |
Hi @yusukebe! The changes you have indicated here are good ones! It will reduce the amount of code and clarify the intent. However, with this change, the following code would have the same level of performance. (It may depend on the runtime environment, though. I did not see any difference when I tried it on Bun). const mwWithAwait = createMiddleware(async (c, next) => {
before()
await next()
after()
})
const mwWithoutAwait = createMiddleware((c, next) => {
before()
return next(after)
}) |
@yusukebe |
I haven't been actively contributing to Hono for a while due to personal life changes. I've been periodically checking the issues in the repository and stumbled upon this discussion. |
Hi @metrue ! This is interesting. The facts that we/me and @usualoma discussed is mostly about running apps on Bun. For Bun, On Bun, there is a difference between using But, on Node.js. It's a very few. Recently, we've been always measuring benchmark scores on Bun. As a result, we pay attention to the use of |
Promise itself is just two callbacks for handle things async, I kind of would disagree with change to callback. Promise is very clever (because can be ref) than doing manual callbacks. As example, if I have middleware which is async by nature (singleshot promise) .. now two Requests incoming and if this have callback setup, it means that need to wrap/run extra promises for each request (await before callback()) as we can't anymore pass actual middleware promise as next(). So even though this might give some edge case performance you might acually consume more memory to just avoid await resolve callback as instead have one ref you will have many. As overy simplfied example .. think that middleware chain is just static promise chain, do you like to refer to this single Promise callback for this chain or cause one extra await Promise resolve() which runs before drop to callback() |
I realized that we don't have to write
await
fornext()
in middleware. This means if it does not have async functions in middleware, you can write like the following:If so, this improves performance when using middleware. Using
await
will be slow with many middlewares.I've taken a benchmark. The code is here:
And the result:
We can use
next()
withoutawait
, we just haven't done it that way until now.In this PR, I've added tests for without
await
and fixed the type definitions.Author should do the followings, if applicable
yarn denoify
to generate files for Deno