Skip to content

Commit f4a17eb

Browse files
authored
Ensuring single instance exposed per runtime.
2 parents e77d8f5 + 22113bb commit f4a17eb

File tree

8 files changed

+152
-94
lines changed

8 files changed

+152
-94
lines changed

README.md

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,7 @@ const app = express();
2222
const port = 3000;
2323

2424
const ContextMiddleware = (req, res, next) => {
25-
const requestResource = new AsyncResource('REQUEST_CONTEXT');
26-
requestResource.runInAsyncScope(() => {
27-
Context.create({
28-
val: true
29-
});
30-
next();
31-
});
25+
Context.run(next, { val: true });
3226
};
3327

3428
app.use('/', ContextMiddleware);

example/middleware.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
1-
const { AsyncResource } = require('async_hooks');
21
const Context = require('../src');
32

43
module.exports = (req, res, next) => {
5-
const asyncResource = new AsyncResource('REQUEST_CONTEXT');
6-
return asyncResource.runInAsyncScope(() => {
7-
Context.create({
8-
val: true
9-
});
10-
next();
11-
});
4+
Context.run(next, { val: true });
125
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-execution-context",
3-
"version": "1.0.5",
3+
"version": "1.1.0",
44
"description": "Provides execution context wrapper for node JS, can be used to create execution wrapper for handling requests and more",
55
"author": "Oded Goldglas <odedglas@gmail.com>",
66
"license": "ISC",

src/hooks/constants.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,10 @@
44
* We can skip those types assuming the execution context won't be used for process types.
55
* @type {Set<String>}
66
*/
7-
exports.EXCLUDED_ASYNC_TYPES = new Set(['DNSCHANNEL', 'TLSWRAP', 'TCPWRAP', 'HTTPPARSER']);
7+
exports.EXCLUDED_ASYNC_TYPES = new Set([
8+
'DNSCHANNEL',
9+
'TLSWRAP',
10+
'TCPWRAP',
11+
'HTTPPARSER',
12+
'ZLIB'
13+
]);

src/hooks/index.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,12 @@ const getContextRef = (parentContext, triggerAsyncId) => (
1111
);
1212

1313
/**
14-
* Suspends map entry removal using next tick queue
15-
* @param {ExecutionContextMap} map - The execution context map
16-
* @param {Number} id - The async id to remove
14+
* Suspends a given f unction execution over process next tick.
15+
* @param {Function} fn - The function to trigger upon next tick.
16+
* @param {...any} args - The function arguments to trigger with.
17+
* @return {any}
1718
*/
18-
const suspendedDelete = (map, id) => process.nextTick(
19-
() => map.delete(id)
20-
);
19+
const suspend = (fn, ...args) => process.nextTick(() => fn(...args));
2120

2221
/**
2322
* The "async_hooks" init hook callback, used to initialize sub process of the main context
@@ -59,7 +58,7 @@ const onChildProcessDestroy = (executionContextMap, asyncId, ref) => {
5958

6059
// Parent context will be released upon last child removal
6160
if (!children.length) {
62-
suspendedDelete(executionContextMap, ref);
61+
executionContextMap.delete(ref)
6362

6463
return;
6564
}
@@ -85,14 +84,14 @@ const destroy = (executionContextMap) => (asyncId)=> {
8584

8685
// Child context's will unregister themselves from root context
8786
if (!isUndefined(ref)) {
88-
onChildProcessDestroy(
87+
suspend(
88+
onChildProcessDestroy,
8989
executionContextMap,
9090
asyncId,
91-
ref,
91+
ref
9292
);
9393
}
94-
95-
suspendedDelete(executionContextMap, asyncId);
94+
executionContextMap.delete(asyncId);
9695
};
9796

9897

src/index.js

Lines changed: 91 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,9 @@
11
const asyncHooks = require('async_hooks');
2+
const ExecutionContextResource = require('./lib/ExecutionContextResource')
23
const { isProduction } = require('./lib');
34
const { create: createHooks } = require('./hooks');
45
const { ExecutionContextErrors } = require('./constants');
56

6-
/**
7-
* The global service context execution map
8-
* @type ExecutionContextMap
9-
*/
10-
const executionContextMap = new Map();
11-
12-
// Sets node async hooks setup
13-
asyncHooks.createHook(
14-
createHooks(executionContextMap)
15-
).enable();
16-
177
/**
188
* Handles execution context error, throws when none production
199
* @param code
@@ -28,65 +18,100 @@ const handleError = (code) => {
2818

2919
/**
3020
* The Execution Context API
21+
* @return {ExecutionContextAPI}
3122
*/
32-
const Context = {
23+
const createExecutionContext = () => {
3324

3425
/**
35-
* Creates an execution context for the current asyncId process.
36-
* This will expose Context get / update at any point after.
37-
* @param {Object} initialContext - The initial context to be used
38-
* @returns void
26+
* The global service context execution map
27+
* @type ExecutionContextMap
3928
*/
40-
create: (initialContext = {}) => {
41-
const asyncId = asyncHooks.executionAsyncId();
42-
43-
// Creation is allowed once per execution context
44-
if (executionContextMap.has(asyncId)) handleError(ExecutionContextErrors.CONTEXT_ALREADY_DECLARED);
45-
46-
executionContextMap.set(asyncId, {
47-
context: { ...initialContext, executionId: asyncId },
48-
children: []
49-
});
50-
},
51-
52-
/**
53-
* Updates the current async process context
54-
* @param {Object} update - The update to apply on the current process context
55-
* @returns void
56-
*/
57-
update: (update = {}) => {
58-
const asyncId = asyncHooks.executionAsyncId();
59-
60-
if (!executionContextMap.has(asyncId)) handleError(ExecutionContextErrors.CONTEXT_DOES_NOT_EXISTS);
61-
62-
const contextData = executionContextMap.get(asyncId);
63-
64-
// Update target is always the root context, ref updates will need to be channeled
65-
const targetContextData = contextData.ref
66-
? executionContextMap.get(contextData.ref)
67-
: contextData;
68-
69-
targetContextData.context = { ...targetContextData.context, ...update };
70-
},
71-
72-
/**
73-
* Gets the current async process execution context
74-
* @returns {Object}
75-
*/
76-
get: () => {
77-
const asyncId = asyncHooks.executionAsyncId();
78-
if (!executionContextMap.has(asyncId)) handleError(ExecutionContextErrors.CONTEXT_DOES_NOT_EXISTS);
79-
80-
const { context = {}, ref } = executionContextMap.get(asyncId);
81-
if (ref) {
82-
83-
// Ref will be used to point out on the root context
84-
return executionContextMap.get(ref).context;
29+
const executionContextMap = new Map();
30+
31+
// Sets node async hooks setup
32+
asyncHooks.createHook(
33+
createHooks(executionContextMap)
34+
).enable();
35+
36+
const Context = {
37+
38+
/**
39+
* Creates an execution context for the current asyncId process.
40+
* This will expose Context get / update at any point after.
41+
* @param {Object} initialContext - The initial context to be used
42+
* @returns void
43+
*/
44+
create: (initialContext = {}) => {
45+
console.log('Ceating: ', executionContextMap);
46+
const asyncId = asyncHooks.executionAsyncId();
47+
48+
// Creation is allowed once per execution context
49+
if (executionContextMap.has(asyncId)) handleError(ExecutionContextErrors.CONTEXT_ALREADY_DECLARED);
50+
51+
executionContextMap.set(asyncId, {
52+
context: { ...initialContext, executionId: asyncId },
53+
children: []
54+
});
55+
},
56+
57+
/**
58+
* Updates the current async process context.
59+
* @param {Object} update - The update to apply on the current process context.
60+
* @returns void
61+
*/
62+
update: (update = {}) => {
63+
const asyncId = asyncHooks.executionAsyncId();
64+
65+
if (!executionContextMap.has(asyncId)) handleError(ExecutionContextErrors.CONTEXT_DOES_NOT_EXISTS);
66+
67+
const contextData = executionContextMap.get(asyncId);
68+
69+
// Update target is always the root context, ref updates will need to be channeled
70+
const targetContextData = contextData.ref
71+
? executionContextMap.get(contextData.ref)
72+
: contextData;
73+
74+
targetContextData.context = { ...targetContextData.context, ...update };
75+
},
76+
77+
/**
78+
* Gets the current async process execution context.
79+
* @returns {Object}
80+
*/
81+
get: () => {
82+
const asyncId = asyncHooks.executionAsyncId();
83+
if (!executionContextMap.has(asyncId)) handleError(ExecutionContextErrors.CONTEXT_DOES_NOT_EXISTS);
84+
85+
const { context = {}, ref } = executionContextMap.get(asyncId);
86+
if (ref) {
87+
88+
// Ref will be used to point out on the root context
89+
return executionContextMap.get(ref).context;
90+
}
91+
92+
// Root context
93+
return context;
94+
},
95+
96+
/**
97+
* Runs a given function within "AsyncResource" context, this will ensure the function executed within a uniq execution context.
98+
* @param {Function} fn - The function to run.
99+
* @param {Object} initialContext - The initial context to expose to the function execution
100+
*/
101+
run: (fn, initialContext) => {
102+
const resource = new ExecutionContextResource();
103+
104+
resource.runInAsyncScope(() => {
105+
Context.create(initialContext);
106+
107+
fn();
108+
});
85109
}
110+
};
86111

87-
// Root context
88-
return context;
89-
}
90-
};
112+
return Context;
113+
}
114+
115+
global.ExecutionContext = global.ExecutionContext || createExecutionContext();
91116

92-
module.exports = Context;
117+
module.exports = global.ExecutionContext;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
const { AsyncResource } = require('async_hooks');
2+
3+
const ASYNC_RESOURCE_TYPE = 'REQUEST_CONTEXT';
4+
5+
/**
6+
* Wraps node AsyncResource with predefined type.
7+
*/
8+
class ExecutionContextResource extends AsyncResource {
9+
constructor() {
10+
super(ASYNC_RESOURCE_TYPE);
11+
}
12+
}
13+
14+
module.exports = ExecutionContextResource;

src/types.d.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,30 @@ interface HookCallbacks {
3030
*/
3131
destroy?(asyncId: number): void;
3232
}
33+
34+
interface ExecutionContextAPI {
35+
36+
/**
37+
* Creates an execution context for the current asyncId process.
38+
* @param initialContext
39+
*/
40+
create(initialContext: object): void;
41+
42+
/**
43+
* Updates the current async process context.
44+
* @param update
45+
*/
46+
update(update: object): void;
47+
48+
/**
49+
* Gets the current async process execution context.
50+
*/
51+
get(): object
52+
53+
/**
54+
* Runs a given function within an async resource context
55+
* @param fn
56+
* @param initialContext
57+
*/
58+
run(fn: Function, initialContext: object): void
59+
}

0 commit comments

Comments
 (0)