Skip to content

Commit 74e6bfd

Browse files
authored
Ability to run Tasks.js and Targets.js code without Nools (medic#8065)
This is a proposed change which allows CHT Application code to run without the Nools library. - This provides a workaround for medic#6506 and medic/cht-conf#225 - Step in the direction of medic#5906. When ready we can remove the nools.emitter - Probable minor performance gains since nools is a weird box of mysteries. May improve medic#5515 if that issue is correct about Nools holding target instances in memory.
1 parent e3aec1e commit 74e6bfd

16 files changed

+1103
-819
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "medic",
3-
"version": "4.1.0",
3+
"version": "4.2.0",
44
"private": true,
55
"license": "AGPL-3.0-only",
66
"repository": {

shared-libs/rules-engine/src/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ module.exports = db => {
2222
* @param {Object[]} settings.targets Target definitions from settings doc
2323
* @param {Boolean} settings.enableTasks Flag to enable tasks
2424
* @param {Boolean} settings.enableTargets Flag to enable targets
25+
* @param {Boolean} [settings.rulesAreDeclarative=false] Flag to indicate the content of settings.rules. When true,
26+
* rules is processed as native JavaScript. When false, nools is used.
27+
* @param {RulesEmitter} [settings.customEmitter] Optional custom RulesEmitter object
2528
* @param {Object} settings.contact User's hydrated contact document
2629
* @param {Object} settings.user User's settings document
2730
*/
@@ -79,6 +82,9 @@ module.exports = db => {
7982
* @param {Object[]} settings.targets Target definitions from settings doc
8083
* @param {Boolean} settings.enableTasks Flag to enable tasks
8184
* @param {Boolean} settings.enableTargets Flag to enable targets
85+
* @param {Boolean} [settings.rulesAreDeclarative=false] Flag to indicate the content of settings.rules. When true,
86+
* rules is native JavaScript. When false, nools is used
87+
* @param {RulesEmitter} [settings.customEmitter] Optional custom RulesEmitter object
8288
* @param {Object} settings.contact User's hydrated contact document
8389
* @param {Object} settings.user User's user-settings document
8490
*/

shared-libs/rules-engine/src/provider-wireup.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ module.exports = {
2525
* @param {Object[]} settings.targets Target definitions from settings doc
2626
* @param {Boolean} settings.enableTasks Flag to enable tasks
2727
* @param {Boolean} settings.enableTargets Flag to enable targets
28+
* @param {Boolean} [settings.rulesAreDeclarative=true] Flag to indicate the content of settings.rules. When true,
29+
* rules is processed as native JavaScript. When false, nools is used.
30+
* @param {RulesEmitter} [settings.customEmitter] Optional custom RulesEmitter object
2831
* @param {number} settings.monthStartDate reporting interval start date
2932
* @param {Object} userDoc User's hydrated contact document
3033
*/
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* @module emitter.javascript
3+
* Processes declarative configuration code by executing javascript rules directly
4+
*/
5+
6+
class Contact {
7+
constructor({ contact, reports, tasks}) {
8+
this.contact = contact;
9+
this.reports = reports;
10+
this.tasks = tasks;
11+
}
12+
}
13+
14+
// required by marshalDocsByContact
15+
Contact.prototype.tasks = 'defined';
16+
17+
class Task {
18+
constructor(x) {
19+
Object.assign(this, x);
20+
}
21+
}
22+
23+
class Target {
24+
constructor(x) {
25+
Object.assign(this, x);
26+
}
27+
}
28+
29+
let processDocsByContact;
30+
const results = { tasks: [], targets: [] };
31+
32+
module.exports = {
33+
getContact: () => Contact,
34+
initialize: (settings, scope) => {
35+
const rawFunction = new Function('c', 'Task', 'Target', 'Utils', 'user', 'cht', 'emit', settings.rules);
36+
processDocsByContact = container => rawFunction(
37+
container,
38+
Task,
39+
Target,
40+
scope.Utils,
41+
scope.user,
42+
scope.cht,
43+
emitCallback,
44+
);
45+
return true;
46+
},
47+
48+
startSession: () => {
49+
if (!processDocsByContact) {
50+
throw Error('Failed to start task session. Not initialized');
51+
}
52+
53+
results.tasks = [];
54+
results.targets = [];
55+
56+
return {
57+
processDocsByContact,
58+
result: () => Promise.resolve(results),
59+
dispose: () => {},
60+
};
61+
},
62+
63+
isLatestNoolsSchema: () => true,
64+
65+
shutdown: () => {
66+
processDocsByContact = undefined;
67+
},
68+
};
69+
70+
const emitCallback = (instanceType, instance) => {
71+
if (instanceType === 'task') {
72+
results.tasks.push(instance);
73+
} else if (instanceType === 'target') {
74+
results.targets.push(instance);
75+
}
76+
};
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @module emitter.nools
3+
* Encapsulates interactions with the nools library
4+
* Promisifies the execution of partner "rules" code
5+
* Ensures memory allocated by nools is freed after each run
6+
*/
7+
const nools = require('nools');
8+
9+
let flow;
10+
11+
const startSession = function() {
12+
if (!flow) {
13+
throw Error('Failed to start task session. Not initialized');
14+
}
15+
16+
const session = flow.getSession();
17+
const tasks = [];
18+
const targets = [];
19+
session.on('task', task => tasks.push(task));
20+
session.on('target', target => targets.push(target));
21+
22+
return {
23+
assert: session.assert.bind(session),
24+
dispose: session.dispose.bind(session),
25+
26+
// session.match can return a thenable but not a promise. so wrap it in a real promise
27+
match: () => new Promise((resolve, reject) => {
28+
session.match(err => {
29+
session.dispose();
30+
if (err) {
31+
return reject(err);
32+
}
33+
34+
resolve({ tasks, targets });
35+
});
36+
}),
37+
};
38+
};
39+
40+
module.exports = {
41+
getContact: () => flow.getDefined('contact'),
42+
initialize: (settings, scope) => {
43+
flow = nools.compile(settings.rules, {
44+
name: 'medic',
45+
scope,
46+
});
47+
48+
return !!flow;
49+
},
50+
startSession: () => {
51+
const session = startSession();
52+
return {
53+
processDocsByContact: session.assert,
54+
dispose: session.dispose,
55+
result: session.match,
56+
};
57+
},
58+
59+
/**
60+
* When upgrading to version 3.8, partners are required to make schema changes in their partner code
61+
* https://docs.communityhealthtoolkit.org/core/releases/3.8.0/#breaking-changes
62+
*
63+
* @returns True if the schema changes are in place
64+
*/
65+
isLatestNoolsSchema: () => {
66+
if (!flow) {
67+
throw Error('task emitter is not enabled -- cannot determine schema version');
68+
}
69+
70+
const Task = flow.getDefined('task');
71+
const Target = flow.getDefined('target');
72+
const hasProperty = (obj, attr) => Object.hasOwnProperty.call(obj, attr);
73+
return hasProperty(Task.prototype, 'readyStart') &&
74+
hasProperty(Task.prototype, 'readyEnd') &&
75+
hasProperty(Target.prototype, 'contact');
76+
},
77+
78+
shutdown: () => {
79+
nools.deleteFlows();
80+
flow = undefined;
81+
},
82+
};

0 commit comments

Comments
 (0)