Introduction
The macchina.io EDGE JavaScript programming environment provides access to various global objects and functions, which give scripts access to the features of macchina.io EDGE.
These are described in the following, along with example code.
Furthermore, macchina.io EDGE supports a simple module system for JavaScript that allows a script to import a "module", typically containing re-usable code. The module system is based on the modules specification from CommonJS.
This document assumes you are already familiar with JavaScript and its concepts.
JavaScript Bundles
An important question is how to actually get JavaScript code into macchina.io EDGE. In general, all JavaScript code executed within macchina.io EDGE must be contained in bundles. There are different ways to create a bundle and deploy it into a macchina.io EDGE server. The easiest way is to use the Playground app in the web user interface. It provides a browser-based editor that allows you to create and edit JavaScript scripts, as well as to execute them in macchina.io EDGE. This is great for trying out things very quickly. The Playground can also be used to create and download a complete bundle containing the script. This bundle can then be deployed to a macchina.io EDGE based device, either by placing it on the filesystem (in one of the directories configured as bundle repositories), or by installing them using the web based Bundle utility.
To learn more about bundles, please refer to the Open Service Platform documentation, specifically the OSP Overview and OSP Bundles documents.
In macchina.io EDGE, there are two ways that JavaScript code can be executed. The first is by placing one or more scripts in a bundle, and by creating one or more JavaScript extensions in the bundle. The extension basically tells macchina.io EDGE which scripts to automatically run when the bundle is started. Every script referenced in a JavaScript extension runs in its own thread. Different scripts can also share code, by placing code in modules and importing them via the require() function. See the following section on modules for more information.
The other way to execute JavaScript is via the web server. Any files served by the web server with the extension ".jss" or ".jssp" will be treated as JavaScript servlets, or server pages, and executed when requested by a client.
The JavaScript Extension Point
The JavaScript extension point in macchina.io EDGE tells the macchina.io EDGE server which scripts (contained in the bundle) to automatically execute when the bundle starts. Each script is executed in its own thread. Optionally, the extension point can also specify a limit to the JavaScript heap, thus preventing the script from using too much memory. A list of search paths can also be given.
By convention, extensions are specified using an XML file named "extensions.xml", located in the root directory of a bundle.
Here's an example what the "extensions.xml" looks like to run a script in a file named "main.js", also located in the root directory of the bundle.
<extensions> <extension point="com.appinf.osp.js" script="main.js"/> </extensions>
The script will run when the bundle is started, and it will be stopped when the bundle is stopped.
To set a memory limit for the script, use the memoryLimit attribute to specify the allowed amount of memory in bytes. Example:
<extensions> <extension point="com.appinf.osp.js" script="main.js" memoryLimit="500000"/> </extensions>
A comma or semicolon-separated list of search paths/URIs for imported modules can be specified using the searchPaths attribute.
There are actually no special tools required to create a bundle. In the simplest form, a bundle is just a directory with specific contents, located in one of the directories specified as bundle repositories. Since directories are hard to deploy, such a bundle directory is usually stored in a ZIP archive, using the ".bndl" extension. This is called a bundle file.
Creating a JavaScript Bundle From Scratch
Let's now create a bundle from scratch. Every bundle needs to contain some meta information that tells macchina.io EDGE (or more specifically, the underlying Open Service Platform) something about the bundle (e.g., its name, its creator, its dependencies, etc.). This information is contained in a text file named "manifest.mf", which must be located in a special directory named "META-INF", located in the root of the bundle directory.
The directory hierarchy of a bundle containing a script so basically looks like this:
/ (bundle root directory) META-INF/ manifest.mf extensions.xml main.js
The "manifest.mf" file for a simple bundle looks as follows:
Manifest-Version: 1.0 Bundle-Name: My First Bundle Bundle-SymbolicName: com.my-company.my-first-bundle Bundle-Version: 1.0.0 Bundle-Vendor: My Company Bundle-Copyright: (c) 2015, My Company Bundle-RunLevel: 900 Bundle-LazyStart: false
When creating your own bundle, make sure not to change the values for Manifest-Version, Bundle-RunLevel, or Bundle-LazyStart (unless you are already familiar enough with OSP to know what you're doing).
By convention, the symbolic name of a bundle (the name used internally) is written in reverse domain name notation, to ensure bundle names are unique, even if bundles come from different vendors.
For more detailed information regarding the bundle manifest, please refer to the OSP Bundles manual.
We've already shown the content of the "extensions.xml" file for the bundle. Here it is again for reference:
<extensions> <extension point="com.appinf.osp.js" script="main.js"/> </extensions>
Finally, here is a simple "Hello, world!" script for "main.js":
logger.information('Hello, world!');
The easiest way to get such a bundle file is to use the Playground's "Export Bundle" button. This will download a ".bndl" file. You can use an unzip tool like unzip on Linux or macOS to extract the files contained in the bundle. Use this as a starting point for your own bundles.
Please make sure that when creating a bundle manually using the zip tool that there is no top-level directory in the Zip file. The "META-INF" directory and "extensions.xml" file must be at the top-level of the Zip archive. Furthermore, the name of the bundle file must conform to the convention "<symbolic-name>_<version>.bndl", e.g. "com.my-company.my-first-bundle_1.0.0.bndl".
One way to ensure this is to cd into the bundle directory, then invoke zip as in the following:
$ zip -r ../com.my-company.my-first-bundle_1.0.0.bndl *
Creating a JavaScript Bundle With The BundleCreator Tool
For creating bundles, macchina.io EDGE provides the Bundle Creator (bundle) tool from the Open Service Platform. While creating bundles manually using zip is fine for simple bundles, using the bundle tool makes it easier to create more complex bundles that have dependencies on other bundles, and that need to collect files from different source directories.
For the Bundle Creator tool, it's necessary to describe the metadata and contents of the bundle in a so-called bundle specification file (".bndlspec"). Furthermore, it's good practice to not invoke the Bundle Creator directly, but through a Makefile.
The recommended directory layout for a JavaScript bundle is thus:
/ (bundle project root directory) Makefile MyFirstBundle.bndlspec bundle/ main.js
The Bundle Specification File
The bundle specification file (MyFirstBundle.bndlspec) looks like this:
<?xml version="1.0"?> <bundlespec> <manifest> <name>My First Bundle</name> <symbolicName>com.my-company.my-first-bundle</symbolicName> <version>1.0.0</version> <vendor>My Company</vendor> <copyright>(c) 2015, My Company</copyright> <lazyStart>false</lazyStart> <runLevel>900</runLevel> <dependency> <symbolicName>com.appinf.osp.js</symbolicName> <version>[1.0.0,2.0.0)</version> </dependency> </manifest> <code> </code> <files> bundle/* </files> </bundlespec>
Note that the bundle specification declares a dependency on the com.appinf.osp.js bundle, which contains the JavaScript runtime.
For a detailed description of the bundle specification file format, please see the Bundle Creator documentation.
The Makefile
The Makefile for the bundle makes use of macchina.io EDGE build system based on GNU Make. It looks as follows:
.PHONY: all include $(POCO_BASE)/build/rules/global include $(POCO_BASE)/OSP/BundleCreator/BundleCreator.make all: $(SET_LD_LIBRARY_PATH) $(BUNDLE_TOOL) MyFirstBundle.bndlspec
It first includes the global build system settings, and then definitions for invoking the Bundle Creator tool. Finally, it defines a target that builds the bundle by running the Bundle Creator.
Before invoking the Makefile via GNU Make, the environment variables POCO_BASE and PROJECT_BASE must be defined. POCO_BASE must contain the absolute path of the platform directory within the macchina.io EDGE source tree. PROJECT_BASE must contain the absolute path to the parent directory of your project. If your MyFirstBundle directory is in /home/user/projects/MyFirstBundle, then PROJECT_BASE must be set to /home/user/projects.
Building
Assuming you are in the your MyFirstBundle project directory, run the following commands to build the bundle:
$ export POCO_BASE=/path/to/macchina.io/platform $ export PROJECT_BASE=/home/user/projects $ make
After running make, the bundle file com.my-company.my-first-bundle_1.0.0.bndl will be created in the current directory.
JavaScript Servlets and Server Pages
Scripts can also be executed in order to serve web requests. macchina.io EDGE uses the Open Service Platform Web Server, which gets all content it serves from bundles. To make the web server execute server-side JavaScript, that script must be in a file with extension ".jss" or ".jssp", and the file must be accessible through the web server. There is an important difference between ".jss" and ".jssp" files. The former contain a so-called "JavaScript Servlet", while the latter contain a "JavaScript Server Page".
Servlets
A JavaScript Servlet is a script that will be invoked whenever a client requests the respective file from the web server. The script has access to global request and response objects which are used to process the HTTP request and send an appropriate HTTP response. There are also form, uploads and session objects available, which are used for HTML form processing, including file uploads, as well as user authentication. These objects are described in detail in the reference documentation.
Here's a little sample servlet that generates a "Hello, world!" JSON document:
var hello = { message: "Hello, world!" }; response.contentType = 'application/json'; response.write(JSON.stringify(hello)); response.send();
Server Pages
A JavaScript Server Page looks like an ordinary HTML document, except that it can have embedded JavaScript code using special directives. Such a server page will be compiled into a servlet script when first requested, and then works exactly like a servlet.
Here's a simple example for a server page:
<% var now = new DateTime(); %> <html> <head> <title>DateTime Sample</title> </head> <body> <h1>DateTime Sample</h1> <p><%= now.toString() %></p> </body> </html>
As you can see, the special directive:
<% <script> %>
is used to enclose JavaScript code.
The special directive:
<%= <expression> %>
is used to embed a JavaScript expression. The result of evaluating the expression is directly inserted into the resulting document, with HTML reserved characters (<, >, ", &) properly escaped.
There's also:
<%- <expression> %>
which is similar to the above, but inserts the result of evaluating the expression as is (no escaping).
JavaScript Server Page Syntax
The following special tags are supported in a JavaScript server page (JSSP) file.
Hidden Comment
A hidden comment documents the JSSP file, but is not sent to the client.
<%-- <comment> --%>
Expression (escaped)
The result of any valid JavaScript expression can be directly inserted into the page, but with reserved HTML characters (<, >, ", &) replaced by their character entities. Note that the expression must not end with a semicolon.
<%= <expression> %>
Expression (verbatim)
The result of any valid JavaScript expression can be directly inserted into the page. No escaping of reserved HTML characters is done. Note that the expression must not end with a semicolon.
<%- <expression> %>
Scriptlet
Arbitrary JavaScript code fragments can be included using the Scriptlet directive.
<% <statement> ... %>
Include Directive
Another JSSP file or resource can be included into the current file using the Include Directive.
<%@ include file="<uri>" %>
Alternatively, this can also be written as:
<%@ include page="<uri>" %>
Page Directive
The Page Directive allows the definition of attributes that control various aspects of JavaScript servlet code generation.
<%@ page <attr>="<value>" ... %>
The following attributes are supported:
contentType
This allows setting the Content-Type header of the HTTP response.
<%@ page contentType="application/json" %>
contentLanguage
This allows setting the Content-Language header of the HTTP response.
contentSecurityPolicy
This allows setting the Content-Security-Policy header of the HTTP response.
referrerPolicy
This allows setting the Referrer-Policy header of the HTTP response.
cacheControl
This allows setting the Cache-Control header of the HTTP response.
Modules
macchina.io EDGE supports JavaScript modules. This allows a script to import another script, with the imported script being encapsulated within a "module" and thus not polluting the global namespace.
A script can import another module using the require() function, specifying either a relative path to the script (i.e., in the same bundle, or to a list of search paths/URIs) or a complete URI. This may even be used to download scripts from a web server, using a HTTP URI.
For require() to work, a module must follow certain conventions:
- A module may define global variables. However, these will not be available outside of the module, and thus not pollute the global scope.
- If a module wishes to export an object or function, it has to do so by assigning that object or function to its exports or module.exports object. The module.exports object is the only object available to importers of the module.
- The name of the resource containing the module must have the extension ".js".
Here's a quick example. Say we want to write a function that looks for a temperature sensor, and returns the sensor object, if one if found:
function findTemperatureSensor() { var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"'); if (temperatureRefs.length > 0) return temperatureRefs[0].instance(); else return null; }
You find that this is a useful function, and you want to use it in different scripts. Now you could copy and paste the function into every script where you need it. For obvious reasons, code sharing by copy-and-paste is a bad idea, so you decide to make the function reusable in the form of a module.
To create a re-usable module that exports the findTemperatureSensor() function, the code needs to be changed as follows:
exports.findTemperatureSensor = function() { var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"'); if (temperatureRefs.length > 0) return temperatureRefs[0].instance(); else return null; };
What we've essentially done is we've added the function to the module's exports object. Assuming we've put the above function in a file named "sensors.js", whenever we need this function in a script, we can write:
var sensors = require('sensors'); var temperatureSensor = sensors.findTemperatureSensor();
Instead of adding properties to the exports object, we can also export an entire object. However, this cannot be done by assigning an object to the exports object. The exports object is basically just a reference to the exports property in the global module object. If we assign an object directly to exports, the global exports variable will now reference the object we've assigned. However, the exports property in the module object will still have its old value. Since it is the exports property in the module object that counts, this will not work. We can, however, directly assign an object to the exports property of the module object, in order to export an entire object.
Say, we now want to extend our "sensors.js" module to include support for an ambient light sensor. We can do this as follows:
module.exports = { findTemperatureSensor: function() { var temperatureRefs = serviceRegistry.find('io.macchina.physicalQuantity == "temperature"'); if (temperatureRefs.length > 0) return temperatureRefs[0].instance(); else return null; }, findAmbientLightSensor: function() { var ambientLightRefs = serviceRegistry.find('io.macchina.physicalQuantity == "illuminance"'); if (ambientLightRefs.length > 0) return ambientLightRefs[0].instance(); else return null; } };
Note that require() will make sure that every module is actually loaded and executed at most once, even if it is imported by multiple modules in an import hierarchy.
require() can be used to import modules from other bundles, or even from the web. To import a module from another bundle, use the "bndl://" URI scheme and specify the symbolic name of the bundle, as well as the file name (and path) of the module in the bundle.
Example:
var module = require('bndl://com.mycompany.mybundle/mymodule.js');
Finding Modules
Modules imported with require() will be searched in the following locations and order:
- If a fully qualified URI is specified, the module is loaded from that URI.
- The given (relative) path will be resolved against the URI of the importing module.
- The given (relative) path will be resolved against the search paths specified for the script (using the searchPaths extension point attribute).
- The given (relative) path will be resolved against the global module search paths.
Global Module Search Paths
A global search path for JavaScript modules can be set in the application's configuration file, using the osp.js.moduleSearchPaths configuration property. Multiple paths must be separated with a comma or semicolon.