Skip to content

Commit afe1016

Browse files
authored
Version 2.0.0 (#7)
1 parent 3fc8701 commit afe1016

File tree

19 files changed

+451
-270
lines changed

19 files changed

+451
-270
lines changed

.eslintignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
dist
2-
node_modules
2+
node_modules
3+
example

.eslintrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ module.exports = {
1111
es6: true
1212
},
1313
rules: {
14-
'no-console': 0,
14+
'no-console': 2,
1515
'no-var': 2,
1616
'prefer-const': 2,
1717
semi: 2,

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# CHANGELOG
2+
3+
## 2.0.0 (September 19, 2020)
4+
5+
### New features
6+
7+
- Domains - allow creating a domain under a certain context to split the chain into a standalone context root (parent chain will not depend on its completion for release).
8+
- Configurations settings.
9+
10+
### Improvements
11+
12+
- Favoring direct assign over spread calls.
13+
- Monitor now is controlled by config, will not enrich execution context nodes by default.

README.md

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,13 @@ export class UserController {
5757

5858
## API
5959

60-
### create(initialContext?: object)
60+
### create(initialContext?: object, domain? :string)
6161

6262
Creates for the current async resource an execution context entry identified with his asyncId.
6363
Any future processes that will be added to the async execution chain will be exposed to this context.
6464

65+
> When passing custom domain to this method, the trigger point and all of it's sub processes will be exposed to a standalone context won't effect / be effected by root context.
66+
6567
### update(update: object)
6668

6769
Updates the current execution context with a given update obect.
@@ -74,12 +76,29 @@ Returns the current execution context identified with the current asyncId.
7476

7577
Runs a given function under a dedicated AsyncResource, exposing given initial context to the process and it's child processes.
7678

79+
### configure(config: ExecutionContextConfig) : void
80+
81+
Configures execution context settings.
82+
7783
### monitor(): ExecutionMapUsage
7884

7985
Returns an monitoring report over the current execution map resources
8086

81-
### API Usage
87+
> Before calling `monitor`, you should `configure` execution context to monitor it's nodes. by default they are kept as lean as possible.
88+
89+
```js
90+
const Context = require('node-execution-context');
91+
92+
// Startup
93+
Context.configure({ monitor: true });
94+
8295

96+
// Later on
97+
const usage = Context.monitor();
98+
console.log(usage); // Prints execution context usage report.
99+
```
100+
101+
### API Usage
83102

84103
```js
85104
const Context = require('node-execution-context');
@@ -111,15 +130,15 @@ Promise.resolve().then(() => {
111130
}, 1000);
112131

113132
console.log(Context.get()); // outputs: {"value": true}
114-
console.log(Context.monitor) // Will result with monitor result
115133
});
116134
});
117135
```
118136

119137
The following errors can be thrown while accessing to the context API :
120138

121-
| Code | when |
139+
| Code | When |
122140
|-|-
123141
| CONTEXT_ALREADY_DECLARED | When trying to `create` execution context, but current async resource already exists.
124142
| CONTEXT_DOES_NOT_EXISTS | When try to `get` / `update` the context, but it yet been created.
143+
| MONITOR_MISS_CONFIGURATION | When try to `monitor` without calling `configure` with monitoring option.
125144

example/controller.js

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,28 @@
11
const Context = require('../src');
22

3-
const delay = (callback) => setTimeout(() => {
3+
const delay = (callback, timeout = 1000) => setTimeout(() => {
44
callback();
5-
}, 2000);
5+
}, timeout);
66

77
class UserController {
88
get(req, res) {
99

1010
delay(() => {
11-
console.log('Callback : ', Context.get());
11+
console.log('Callback : ', Context.get()); // { val: true }
1212
});
1313

14+
// Creates a dedicate domain context ( exclude this following chain from root context )
15+
// Updates mae from domain will not effect root context.
16+
delay(() => {
17+
Context.create({ specific: true }, 'custom-domain');
18+
19+
delay(() => {
20+
console.log('Domain callback ', Context.get()) // { val: true, specific: true }
21+
Context.update({ inner: true });
22+
23+
}, 400);
24+
}, 4000)
25+
1426
res.send(Context.get());
1527
}
1628
}

example/server.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ app.use('/', ContextMiddleware);
1414

1515
app.get('/user', UserController.get);
1616

17-
app.listen(port, function(){
17+
app.listen(port, () => {
1818
console.log('Server is running');
1919
});

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.1.6",
3+
"version": "2.0.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 <[email protected]>",
66
"license": "ISC",

src/ExecutionContext/constants.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* The Execution context error that can be thrown while accessing the execution context.
3+
* @type {Object<String>}
4+
*/
5+
exports.ExecutionContextErrors = {
6+
CONTEXT_ALREADY_DECLARED: 'Execution context is already declared for the default domain, use the domain option to create a separate context.',
7+
CONTEXT_DOES_NOT_EXISTS: 'Execution context does not exists, please ensure to call create/run before.',
8+
MONITOR_MISS_CONFIGURATION: 'Monitoring option is off by default, please call `configure` with the proper options.'
9+
};
10+
11+
/**
12+
* The default configuration to use.
13+
* @type {ExecutionContextConfig}
14+
*/
15+
exports.DEFAULT_CONFIG = {
16+
monitor: false
17+
};
18+
19+
/**
20+
* The default domain to create execution context roots under.
21+
* @type {String}
22+
*/
23+
exports.ROOT_DOMAIN = 'ROOT';

src/ExecutionContext/index.js

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
const asyncHooks = require('async_hooks');
2+
const { isProduction, monitorMap, ExecutionContextResource } = require('../lib');
3+
const { create: createHooks, onChildProcessDestroy } = require('../hooks');
4+
const {
5+
DEFAULT_CONFIG,
6+
ROOT_DOMAIN,
7+
ExecutionContextErrors
8+
} = require('./constants');
9+
10+
/**
11+
* The execution context maps which acts as the execution context in memory storage.
12+
* @see ExecutionContext.monitor
13+
* @type ExecutionContextMap
14+
*/
15+
const executionContextMap = new Map();
16+
17+
/**
18+
* Creates a root execution context node.
19+
* @param {Number} asyncId - The current async id.
20+
* @param {Object} initialContext - The initial context ro provide this execution chain.
21+
* @param {Object} config - The configuration the root context created with.
22+
* @param {Object} domain - The domain to create the execution context under.
23+
* @return {ExecutionContextNode}
24+
*/
25+
const createRootContext = ({ asyncId, initialContext, config, domain = ROOT_DOMAIN }) => ({
26+
asyncId,
27+
domain,
28+
context: { ...initialContext, executionId: asyncId },
29+
children: [],
30+
...config,
31+
...(config.monitor && { created: Date.now() })
32+
});
33+
34+
/**
35+
* Handles execution context error, throws when none production.
36+
* @param {String} code - The error code to log.
37+
*/
38+
const handleError = (code) => {
39+
if (!isProduction()) {
40+
throw code;
41+
}
42+
43+
console.error(code); // eslint-disable-line no-console
44+
};
45+
46+
class ExecutionContext {
47+
constructor() {
48+
this.config = { ...DEFAULT_CONFIG };
49+
50+
// Sets node async hooks setup
51+
asyncHooks.createHook(
52+
createHooks(executionContextMap)
53+
).enable();
54+
}
55+
56+
/**
57+
* Configures current execution context.
58+
* @param {ExecutionContextConfig} config - the configuration to use.
59+
*/
60+
configure(config) {
61+
this.config = config;
62+
}
63+
64+
/**
65+
* Creates an execution context for the current asyncId process.
66+
* This will expose Context get / update at any point after.
67+
* @param {Object} initialContext - The initial context to be used.
68+
* @param {String} domain - The domain the context is created under.
69+
* @returns void
70+
*/
71+
create(initialContext = {}, domain = ROOT_DOMAIN) {
72+
const config = this.config;
73+
const asyncId = asyncHooks.executionAsyncId();
74+
75+
const refContext = executionContextMap.get(asyncId);
76+
if (refContext) {
77+
78+
// Execution context creation is allowed once per domain
79+
if (domain === refContext.domain) return handleError(ExecutionContextErrors.CONTEXT_ALREADY_DECLARED);
80+
81+
// Setting up domain initial context
82+
initialContext = { ...this.get(), ...initialContext };
83+
84+
// Disconnecting current async id from stored parent chain
85+
onChildProcessDestroy(executionContextMap, asyncId, refContext.ref);
86+
}
87+
88+
// Creating root context node
89+
const rootContext = createRootContext({
90+
asyncId,
91+
initialContext,
92+
config,
93+
domain
94+
});
95+
96+
executionContextMap.set(asyncId, rootContext);
97+
}
98+
99+
/**
100+
* Updates the current async process context.
101+
* @param {Object} update - The update to apply on the current process context.
102+
* @returns void
103+
*/
104+
update(update = {}) {
105+
const asyncId = asyncHooks.executionAsyncId();
106+
107+
if (!executionContextMap.has(asyncId)) return handleError(ExecutionContextErrors.CONTEXT_DOES_NOT_EXISTS);
108+
109+
const contextData = executionContextMap.get(asyncId);
110+
111+
// Update target is always the root context, ref updates will need to be channeled
112+
const targetContextData = contextData.ref
113+
? executionContextMap.get(contextData.ref)
114+
: contextData;
115+
116+
targetContextData.context = { ...targetContextData.context, ...update };
117+
}
118+
119+
/**
120+
* Gets the current async process execution context.
121+
* @returns {Object}
122+
*/
123+
get() {
124+
const asyncId = asyncHooks.executionAsyncId();
125+
if (!executionContextMap.has(asyncId)) return handleError(ExecutionContextErrors.CONTEXT_DOES_NOT_EXISTS);
126+
127+
const { context = {}, ref } = executionContextMap.get(asyncId);
128+
if (ref) {
129+
130+
// Ref will be used to point out on the root context
131+
return executionContextMap.get(ref).context;
132+
}
133+
134+
// Root context
135+
return context;
136+
}
137+
138+
/**
139+
* Runs a given function within "AsyncResource" context, this will ensure the function executed within a uniq execution context.
140+
* @param {Function} fn - The function to run.
141+
* @param {Object} initialContext - The initial context to expose to the function execution.
142+
* @param {String} domain - The domain to create the exectuion context under.
143+
*/
144+
run(fn, initialContext, domain) {
145+
const resource = new ExecutionContextResource();
146+
147+
resource.runInAsyncScope(() => {
148+
this.create(initialContext, domain);
149+
150+
fn();
151+
});
152+
}
153+
154+
/**
155+
* Monitors current execution map usage
156+
* @return {ExecutionMapUsage}
157+
*/
158+
monitor() {
159+
if (!this.config.monitor) {
160+
throw new Error(ExecutionContextErrors.MONITOR_MISS_CONFIGURATION);
161+
}
162+
163+
return monitorMap(executionContextMap);
164+
}
165+
}
166+
167+
module.exports = ExecutionContext;

src/spec.js renamed to src/ExecutionContext/spec.js

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
const asyncHooks = require('async_hooks');
2-
const hooks = require('./hooks');
2+
const hooks = require('../hooks');
33
const { ExecutionContextErrors } = require('./constants');
44

55
describe('Context', () => {
66
let Context;
7-
beforeEach(() => Context = jest.requireActual('.'));
7+
beforeEach(() => {
8+
const ExecutionContext = jest.requireActual('.');
9+
Context = new ExecutionContext();
10+
});
811

912
describe('Initialise node "async_hooks"', () => {
1013
const spies = {
@@ -116,8 +119,11 @@ describe('Context', () => {
116119
Context.run(execute, initialContext);
117120
});
118121

119-
it('Creates context', () => {
120-
expect(spies.contextCreate).toHaveBeenCalledWith(initialContext);
122+
it('Creates context under root domain', () => {
123+
expect(spies.contextCreate).toHaveBeenCalledWith(
124+
initialContext,
125+
undefined
126+
);
121127
});
122128

123129
it('Executes given function', () => {
@@ -212,5 +218,33 @@ describe('Context', () => {
212218
});
213219
});
214220
});
221+
222+
describe('Domains', () => {
223+
it('Blocks when creation is made under the same domain', () => {
224+
create();
225+
226+
expect(create).toThrow();
227+
});
228+
229+
it('Allows to create sub domains under a root context', (done) => {
230+
create();
231+
232+
expect(Context.get().hey).toBeTruthy();
233+
234+
setTimeout(() => {
235+
Context.create({ some: 'where' }, 'that-domain');
236+
Context.update({ hey: false });
237+
238+
expect(Context.get().hey).toBeFalsy();
239+
expect(Context.get().some).toEqual('where');
240+
}, 500);
241+
242+
setTimeout(() => {
243+
expect(Context.get().hey).toBeTruthy();
244+
245+
done();
246+
}, 1500);
247+
});
248+
});
215249
});
216250
});

0 commit comments

Comments
 (0)