Example Script Patterns
By itself, a Reactor script is a blank slate. It's up to the solution developer how best to use it. To get the most out of Reactor, start with a good understanding of the data being collected by Platform resources as part of the solution and how applying transformations or integrating with more inputs/output systems can bring additional insight. Combining items of Thng, action, or product data is one common way to do this.
The examples below detail some example Reactor script architectural patterns. Using one of these patterns as a template can give some benefits in areas such as maintainability, modularity, reusability, and so on.
Single File
The simplest Reactor script pattern is a single file (called main.js
) with a simple package.json
file declaring dependencies
. This is the default when creating a Reactor script through the EVRYTHNG Dashboard, as shown below. You can find the simple example script featured below in the Example Scripts section of the Reactor API Reference.
The advantages of this pattern are that the code is easily accessible, visible, and can be shared among many account Operators. However, any implementation that's more than trivial complexity doesn't suit this limited view. The multi-file bundle pattern can help alleviate these problems.
Note
The
package.json
file must contain at leastevrythng-extended
, otherwise the script will not execute. If the script is originally created from the Dashboard, this is already included and must not be removed. You can confirm this by clicking ‘Show dependencies’ on the application page.
Using Promise Chains
With the requirement for calling the global done()
function to receive any logs (which can be vital to debugging), we recommend you implement the Reactor script using a chain of promises. This is also valuable in terms of catching and propagating errors and brings benefits to all Reactor script developers.
The best way to benefit from this is to finish the chain with a .catch()
and .then()
to finish the script in a consistent way. To handle both native Error
and API errors, it's recommended to use the catch()
example shown below:
const readThngs = () => app.thng().read();
function onActionCreated(event) {
readThngs()
.then(thngs => logger.info(`Read ${thngs.length} Thngs`))
.catch(e => logger.error(e.message || e.errors[0]))
.then(done);
}
The same can be achieved using async
/await
if using Node 10:
const readThngs = () => app.thng().read();
const onActionCreated = async (event) => {
try {
const thngs = await readThngs();
logger.info(`Read ${thngs.length} Thngs`);
} catch (e) {
logger.error(e.message || e.errors[0]);
}
done();
};
As seen above, the evrythng.js SDK makes this pattern very easy to follow, since virtually all of the resources and operation methods return promises. If an error occurs, it's safely logged and done()
is called as required.
Besides updating a single resource, this pattern can also perform several resource updates at once, including any other asynchronous dependencies, and ensure they are all resolved before the end of the script is reported. For example, read some IDs from a Thng’s customFields
and give them all the ‘owned’ tag:
const readOwnedThngs = async (thngId) => {
const thng = await app.thng(thngId).read();
// Create 'ids' filter from list of 'owned' Thng IDs
const params = { ids: thng.customFields.owned.join(',') };
return app.thng().read({ params });
};
const updateThngTags = (thngs) => {
const update = { tags: ['Owned'] };
// Update all Thngs using a list of promises
const promises = thngs.map(thng => thng.update(update));
return Promise.all(promises);
}
// @filter(onActionCreated) action.type=_setOwnedTag
const onActionCreated = async (event) => {
try {
const thngs = await readOwnedThngs(event.action.thng);
await updateThngTags(thngs);
logger.info('Thngs tags updated!');
} catch (e) {
logger.error(e.message || e.errors[0]);
}
done();
};
Another example is in the case where a resource update must wait for an external resource. For instance, updating a Thng description using the quotes.rest API. The example below does this in a manner that uses a promise to incorporate the external asynchronous request as part of the single promise chain, and any errors it throws are caught and logged safely.
Note
Remember to add
request
topackage.json
to fulfill this dependency.
const request = require('request');
const QOTD_URL = 'http://quotes.rest/qod.json?category=inspire';
// Handle web request using a promise
const getQuote = () => new Promise((resolve, reject) => {
// Fetch the quote
request.get(QOTD_URL, (err, response, body) => {
if(err) {
reject(err);
return;
}
const quotes = JSON.parse(body).contents.quotes;
resolve(`"${quotes[0].quote}" -- ${quotes[0].author}`);
});
});
const updateThng = (thngId, quote) =>
app.thng(thngId).update({ description: quote });
// @filter(onActionCreated) action.type=_setQuote
const onActionCreated = async (event) => {
try {
const quote = await getQuote();
await updateThng(event.action.thng, quote);
} catch (e) {
logger.error(e.message || e.errors[0]);
}
done();
};
{
"dependencies": {
"evrythng-extended": "^4.1.0",
"request": "^2.83.0"
}
}
After creating an action on a Thng with action type _setQuote
, the Thng’s description
now contains a suitably inspiring quote:
{
"id": "UHSP3aayq3PYh6wRaDRccqTh",
"createdAt": 1511952386181,
"updatedAt": 1511952959407,
"name": "QuoteThng",
"description": "\"Life is 10% what happens to us and 90% how we react to it.\" -- Dennis P. Kimbro",
"properties": {},
"identifiers": {}
}
Multi-file Bundle
If a single-file Reactor script becomes too large to easily maintain in a single file, it's recommended to split it into multiple files, and upload it as a .zip
bundle instead. This upload can be done easily through the Dashboard or the API.
Besides moving specific modules of functionality into separate files (such as external integration code, or some static data collections), a useful pattern to use is to create file handlers for each action type of property key the script will encounter. This is especially useful if the use case makes use of many of these and allows easy use of promises to facilitate calling the global done()
function once execution is finished.
An example bundle file structure is shown below:
bundle.zip/
package.json
main.js
actions/
registerUser.js
updateUser.js
removeUser.js
In this pattern, the main event handler onActionCreated()
is located in main.js
and uses the type
of the action created to run the appropriate file handler. This allows for an easily expandable solution by simply adding more file handlers for new action types. For example:
const HANDLERS = {
_registerUser: require('./actions/registerUser'),
_updateUser: require('./actions/updateUser'),
_unregisterUser: require('./actions/unregisterUser')
};
const handleAction = (action) => {
const handler = HANDLERS[action.type];
if (!handler) {
throw new Error(`No handler for action type ${action.type}`);
}
return handler(action);
};
const onActionCreated = async (event) => {
try {
await handleAction(event.action);
} catch (e) {
logger.error(e.message || e.errors[0]);
}
done();
};
and the separated handler files that handles all actions of that type
:
module.exports = (action) => {
logger.info('New user!');
// Update Application User tags
const payload = { tags: ['Participating'] };
return app.user(action.user).update(payload);
};
module.exports = (action) => {
logger.info('User updated!');
// Update Application User custom fields
const payload = { customFields: { appVersion: 2 } };
return app.user(action.user).update(payload);
};
module.exports = (action) => {
logger.info('User unregistered!');
// Update Application User tags
const payload = { tags: [] };
return app.user(action.user).update(payload);
};
Rules in the Thng
A more advanced but user-configurable pattern is to specify some simple (or complex) rules on each individual Thng itself. This is useful if each Thng has its own custom behavior, often a result of their owner’s preferences. An example of this could be a custom schedule for a smart device to be automatically turned on or off, which is specified for each unique device.
These preferences can be stored in Thng customFields
and read by a generalized Reactor script that simply processes these values and performs updates to the Thng accordingly. This means that each Thng can have its own behavior when triggering the Reactor script and can be modified by users, without requiring further updates to the script itself.
Example Script - Compute Rules
A simple and reusable example rule processing script is shown below, and includes a few different arithmetic operators. The rules themselves are read from Thng customFields.rules
array and must include the following fields:
inputs
- a pair of existing Thngproperties
keys that are the operands of the operatoroperator
- a key from theOPERATORS
available in the script (see below) that's the arithmetic operator on theinputs
.output
- the property key where the result is stored.
For example, to set a Thng property temp_c_is_safe
, the lessThan
operator is specified, and will use the temp_c
and max_temp_c
as input values. So if the temp_c
is less than max_temp_c
, temp_c_is_safe
will be set to true. This could represent a smart thermostat performing monitoring for the user.
{
"name": "Thng with Rules",
"description": "A Thng with rules in customFields",
"customFields": {
"rules": [
{
"inputs": [ "temp_c", "max_temp_c" ],
"operator": "lessThan",
"output": "temp_c_is_safe"
}
]
}
}
The generalized Reactor script itself is triggered by a Thng property update (such as an increase in temp_c
) and filtered accordingly with Reactor filters. The compute()
function is called, which processes the Thng’s customFields
to extract any rules, and then performs the property updates specified by those rules.
The example OPERATORS
shown can be extended further by the reader, as long as the input property values are Numbers. A further exercise could be to generalize the operators beyond just numbers.
// ------------------------------- Compute Rules -------------------------------
const OPERATORS = {
add: items => items[0] + items[1],
subtract: items => items[0] - items[1],
multiply: items => items[0] * items[1],
divide: items => items[0] / items[1],
equals: items => items[0] === items[1],
lessThan: items => items[0] < items[1]
};
const compute = async (thng) => {
// Any rules?
const { customFields } = thng;
if (!(customFields && customFields.rules)) {
return;
}
// Process all rules as promises
const promises = customFields.rules.map((rule) => {
// Map input property names to their actual values
const inputs = rule.inputs.map(key => parseFloat(thng.properties[key]));
// Perform the computation
const result = OPERATORS[rule.operator](inputs);
logger.info(`${result} << ${JSON.stringify(rule)}`);
// Store the output property value
return app.thng(thng.id).property(rule.output).update(result);
});
return Promise.all(promises);
};
// ------------------------------ Reactor Events -------------------------------
// @filter(onThngPropertiesChanged) propertyChangeNew.temp_c=*
const onThngPropertiesChanged = async (event) => {
try {
await compute(event.thng);
} catch (e) {
logger.error(e.message || e.errors[0]);
}
done();
};
Conclusion
In this section you have learned about some example Reactor script patterns that promote maintainability, separation of concerns, and catching any errors that may be thrown. Some ideas here can be easily reused as starting points for future use-cases and integrations, as well as to build upon to improve your understanding.
In the next section, we walk through building a simple single-file Reactor script from scratch that forms the basis of a simple scan campaign involving accumulating insights and metadata, while using two different Reactor script event types.
Updated almost 2 years ago