Skip to content

Commit 5122a18

Browse files
authored
feat: added AsyncResource factory function option (#173)
1 parent 0a6339a commit 5122a18

5 files changed

Lines changed: 169 additions & 4 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,15 +58,16 @@ fastify.register(fastifyRequestContextPlugin, {
5858
});
5959
```
6060

61-
This plugin accepts options `hook` and `defaultStoreValues`.
61+
This plugin accepts options `hook` and `defaultStoreValues`, `createAsyncResource`.
6262

6363
* `hook` allows you to specify to which lifecycle hook should request context initialization be bound. Note that you need to initialize it on the earliest lifecycle stage that you intend to use it in, or earlier. Default value is `onRequest`.
6464
* `defaultStoreValues` / `defaultStoreValues(req: FastifyRequest)` sets initial values for the store (that can be later overwritten during request execution if needed). Can be set to either an object or a function that returns an object. The function will be sent the request object for the new context. This is an optional parameter.
65+
* `createAsyncResource` can specify a factory function that creates an extended `AsyncResource` object.
6566

6667
From there you can set a context in another hook, route, or method that is within scope.
6768

6869
Request context (with methods `get` and `set`) is exposed by library itself, but is also available as decorator on `fastify.requestContext` app instance as well as on `req` request instance.
69-
70+
7071
For instance:
7172

7273
```js
@@ -77,7 +78,8 @@ const app = fastify({ logger: true })
7778
app.register(fastifyRequestContextPlugin, {
7879
defaultStoreValues: {
7980
user: { id: 'system' }
80-
}
81+
},
82+
createAsyncResource: (req, context) => new MyCustomAsyncResource('custom-resource-type', req.id, context.user.id)
8183
});
8284

8385
app.addHook('onRequest', (req, reply, done) => {

index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ function fastifyRequestContext(fastify, opts, next) {
3434
: opts.defaultStoreValues
3535

3636
asyncLocalStorage.run({ ...defaultStoreValues }, () => {
37-
const asyncResource = new AsyncResource('fastify-request-context')
37+
const asyncResource =
38+
opts.createAsyncResource != null
39+
? opts.createAsyncResource(req, requestContext)
40+
: new AsyncResource('fastify-request-context')
3841
req[asyncResourceSymbol] = asyncResource
3942
asyncResource.runInAsyncScope(done, req.raw)
4043
})

test-tap/requestContextPlugin.e2e.test.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
'use strict'
22

3+
const fastify = require('fastify')
34
const request = require('superagent')
45
const {
56
initAppPostWithPrevalidation,
67
initAppPostWithAllPlugins,
78
initAppGetWithDefaultStoreValues,
89
} = require('../test/internal/appInitializer')
10+
const { fastifyRequestContext } = require('..')
911
const { TestService } = require('../test/internal/testService')
1012
const t = require('tap')
13+
const { CustomResource, AsyncHookContainer } = require('../test/internal/watcherService')
14+
const { executionAsyncId } = require('async_hooks')
1115
const test = t.test
1216

1317
let app
@@ -332,3 +336,34 @@ test('does not throw when accessing context object outside of context', (t) => {
332336
})
333337
})
334338
})
339+
340+
test('passing a custom resource factory function when create as AsyncResource', (t) => {
341+
t.plan(2)
342+
343+
const container = new AsyncHookContainer(['fastify-request-context', 'custom-resource-type'])
344+
345+
app = fastify({ logger: true })
346+
app.register(fastifyRequestContext, {
347+
defaultStoreValues: { user: { id: 'system' } },
348+
createAsyncResource: () => {
349+
return new CustomResource('custom-resource-type', '1111-2222-3333')
350+
},
351+
})
352+
353+
const route = (req) => {
354+
const store = container.getStore(executionAsyncId())
355+
t.equal(store.traceId, '1111-2222-3333')
356+
return Promise.resolve({ userId: req.requestContext.get('user').id })
357+
}
358+
359+
app.get('/', route)
360+
361+
return app.listen({ port: 0, host: '127.0.0.1' }).then(() => {
362+
const { address, port } = app.server.address()
363+
const url = `${address}:${port}`
364+
365+
return request('GET', url).then((response1) => {
366+
t.equal(response1.body.userId, 'system')
367+
})
368+
})
369+
})

test/internal/watcherService.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
'use strict'
2+
3+
const { executionAsyncId, createHook, AsyncResource } = require('node:async_hooks')
4+
const { EventEmitter } = require('node:events')
5+
6+
class CustomResource extends AsyncResource {
7+
constructor(type, traceId) {
8+
super(type)
9+
10+
this.traceId = traceId
11+
}
12+
}
13+
14+
class AsyncWatcher extends EventEmitter {
15+
setupInitHook() {
16+
// init is called during object construction. The resource may not have
17+
// completed construction when this callback runs, therefore all fields of the
18+
// resource referenced by "asyncId" may not have been populated.
19+
this.init = (asyncId, type, triggerAsyncId, resource) => {
20+
this.emit('INIT', {
21+
asyncId,
22+
type,
23+
triggerAsyncId,
24+
executionAsyncId: executionAsyncId(),
25+
resource,
26+
})
27+
}
28+
return this
29+
}
30+
31+
setupDestroyHook() {
32+
// Destroy is called when an AsyncWrap instance is destroyed.
33+
this.destroy = (asyncId) => {
34+
this.emit('DESTROY', {
35+
asyncId,
36+
executionAsyncId: executionAsyncId(),
37+
})
38+
}
39+
return this
40+
}
41+
42+
start() {
43+
createHook({
44+
init: this.init.bind(this),
45+
destroy: this.destroy.bind(this),
46+
}).enable()
47+
48+
return this
49+
}
50+
}
51+
52+
class AsyncHookContainer {
53+
constructor(types) {
54+
const checkedTypes = types
55+
56+
const idMap = new Map()
57+
const resourceMap = new Map()
58+
const watcher = new AsyncWatcher()
59+
const check = (t) => {
60+
try {
61+
return checkedTypes.includes(t)
62+
} catch (err) {
63+
return false
64+
}
65+
}
66+
67+
watcher
68+
.setupInitHook()
69+
.setupDestroyHook()
70+
.start()
71+
.on('INIT', ({ asyncId, type, resource, triggerAsyncId }) => {
72+
idMap.set(asyncId, triggerAsyncId)
73+
74+
if (check(type)) {
75+
resourceMap.set(asyncId, resource)
76+
}
77+
})
78+
.on('DESTROY', ({ asyncId }) => {
79+
idMap.delete(asyncId)
80+
resourceMap.delete(asyncId)
81+
})
82+
83+
this.types = checkedTypes
84+
this.idMap = idMap
85+
this.resourceMap = resourceMap
86+
this.watcher = watcher
87+
}
88+
89+
getStore(asyncId) {
90+
let resource = this.resourceMap.get(asyncId)
91+
92+
if (resource != null) {
93+
return resource
94+
}
95+
96+
let id = this.idMap.get(asyncId)
97+
let sentinel = 0
98+
99+
while (id != null && sentinel < 100) {
100+
resource = this.resourceMap.get(id)
101+
102+
if (resource != null) {
103+
return resource
104+
}
105+
106+
id = this.idMap.get(id)
107+
sentinel += 1
108+
}
109+
110+
return undefined
111+
}
112+
}
113+
114+
module.exports = {
115+
AsyncWatcher,
116+
AsyncHookContainer,
117+
CustomResource,
118+
}

types/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { AsyncResource } from 'async_hooks'
12
import { FastifyPluginCallback, FastifyRequest } from 'fastify'
23

34
type FastifyRequestContext =
@@ -23,6 +24,11 @@ declare namespace fastifyRequestContext {
2324
set<K extends keyof RequestContextData>(key: K, value: RequestContextData[K]): void
2425
}
2526

27+
export type CreateAsyncResourceFactory<T extends AsyncResource = AsyncResource> = (
28+
req: FastifyRequest,
29+
context: RequestContext,
30+
) => T
31+
2632
export type RequestContextDataFactory = (req: FastifyRequest) => RequestContextData
2733

2834
export type Hook =
@@ -43,6 +49,7 @@ declare namespace fastifyRequestContext {
4349
export interface FastifyRequestContextOptions {
4450
defaultStoreValues?: RequestContextData | RequestContextDataFactory
4551
hook?: Hook
52+
createAsyncResource?: CreateAsyncResourceFactory
4653
}
4754

4855
export const requestContext: RequestContext

0 commit comments

Comments
 (0)