Components
A Slipway Component takes an input JSON structure and produces an output JSON structure.
Components can be written in either Javascript or any language that compiles to WASM. Languages have different strengths and weaknesses, as do their ecosystems. With Slipway you can mix and match.
Components are sandboxed by default and cannot do anything except execute their own code and access their own input unless given additional permissions.
With appropriate permissions they become much more powerful, as we'll describe below.
A component is structured as either a folder or a TAR file that contains a slipway_component.json
configuration file,
and any other supporting files required for the Component's execution, such as a run.js
or a run.wasm
file.
Configuration
Components are configured through their slipway_component.json
file. This file has the following structure:
{
"publisher": "...",
"name": "...",
"version": "...",
"description": "...",
"input": {
// ...
},
"output": {
// ...
},
"callouts": {
// ...
},
"rigging": {
// ...
}
"constants": {
// ...
},
}
publisher
, name
, version
Together these fields identify a Component. The format of a Component Reference in a Component Registry
is publisher.name.version
.
Both publisher
and name
must consist of only lowercase alphanumeric characters plus underscore.
The Slipway Component Registry
The Slipway Component Registry is a thin wrapper over GitHub Releases.
By following a simple convention with your publisher
, name
and version
fields,
and your GitHub repository name and release names, your Components will be automatically available for anyone to
use through the Slipway Component Registry when published on GitHub.
See here for more information.
description
The description can contain a short description of what your component does.
input
and output
These fields specify schemas used to validate the input and output data of your components.
If you wish to allow any input, or any output, then you can set the appropriate field to an empty object:
"input": {},
"output": {}
However, providing an input schema not only gives you a guarantee that the data is in the format you expect before your code is executed, but will also give your users better error messages if they provide data in the wrong format. This is especially important if your component is written in a dynamically typed language such as Javascript.
Providing an output schema is useful for ensuring that your Component's code is doing what you expect.
JsonTypeDef or JSON Schema
You can use either JsonTypeDef or JSON Schema to define your schemas.
If the schema contains a root field $schema
with a value containing the json-schema.org
domain, it is assumed to be JSON Schema.
Otherwise it is assumed to be JsonTypeDef.
JsonTypeDef is simpler, and has better defaults, and is the recommended one to use. JSON Schema has more advanced options but is more complex and error-prone.
Examples
Let's say you wanted to specify that your output contained a single value
field which contains an integer. The value
field is required, and
no other fields will be present. It may look something like this:
{
"value": 123
}
The JsonTypeDef schema would be defined as:
"output": {
"properties": {
"value": {
"type": "int32"
}
}
}
The JSON Schema would be defined as:
"output": {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"value": {
"type": "integer"
}
},
"required": ["value"],
"additionalProperties": false
},
With JSON Schema you can also use $ref to refer to other schemas within the Component, for example:
"output": {
"$schema": "http://json-schema.org/draft-07/schema#",
"$ref": "output_schema.json"
},
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"value": {
"type": "integer"
}
},
"required": ["value"],
"additionalProperties": false
}
You cannot refer to schemas outside of the component (for example, using URLs). This is because what constitutes valid data for a component should be fixed with the version of the Component, and referencing external schemas could cause validation to change over time. However you can download these schemas and release them as part of your component instead.
callouts
As mentioned on the Rigs page, some Components will, during their execution, call other Components.
If the Components that will be called are known in advance then they can be specified here, and do not need to be specified in the Rig.
The callout for a given Component Handle is structured as follows:
"callout_handle_one": {
"component": "...",
"allow": [
// ...
],
"deny": [
// ...
],
}
The component
, allow
and deny
fields are the same as those defined for a component in the rigging above.
See the component,
allow and deny sections for more information.
For example if your Component calls out to the slipwayhq.echarts
Component you might specify:
"callouts": {
"echarts": {
"component": "slipwayhq.echarts.0.5.4",
"allow": [
{ "permission": "fonts" },
{ "permission": "registry_components" }
]
}
}
In the above example the Component Handle echarts
has been given to the reference
slipwayhq.echarts.0.5.4
. With this specified, the Component will be able to use the Host API to execute
the Component using its handle.
Note that the Component itself will still need to be granted permission to use these components by the Rig.
rigging
A Component can contain it's own rigging in addition to, or instead of, code to run. This rigging behaves similarly to the rigging of a Rig. The Component's code, if any, is executed first, followed by the rigging.
If the Component contains rigging, then the output schema should reflect the output of the rigging, as it is the result of executing the rigging which is returned from the Component.
The rigging can access the Component's input through a virtual component handle called input
.
For example the query $.input.width
would fetch the value of the input width
property.
The rigging can access the result of the Component's run
function through the run
property
on the input
virtual component handle.
For example the query $.input.run.width
would access the width
property from the result that was
returned by the Component's run
function.
A common pattern is where a Component's run
function fetches, formats and returns some data,
and the rigging then passes that data through an appropriate renderer to produce a Canvas.
For example, the Component's run
function may fetch solar data for a house and output an
ECharts definition.
The Component's rigging will then pass the resulting ECharts definition through to the
slipwayhq.echarts
Component,
so that the Component returns a rendered chart.
constants
Optional. This can contain any arbitrary data structure.
It is a convenient place to put static data which will be referenced by multiple Components within the Component's own rigging.
The Host API
There are two ways components can get access to useful information:
The first is to pass the information in as part of its input JSON structure. A simple data processing Component would typically access its data in this way.
The second is to request data from the Slipway Host using the Host API. The Host API contains a restricted set of APIs which allow a Component to request data from the outside world. However, every request is checked against a chain of permissions to ensure the Component is allowed to make the requests it's trying to make.
The Host API enables components to make HTTP requests, access local files, read local environment variables, query local fonts, and even run other Components. But only if given permission to do so. We will talk about the Host API more in the next section.
Types of Component
Currently Slipway supports four types of Component:
- Special Components
- Fragment Components
- Javascript Components
- WASM Components
Special Components
These not very interesting, but included for completeness.
They comprise of sink
, which takes any input and produces no output, and passthrough
which takes any
input and passes it straight through to the output.
Special components are built-in and can be referenced by just their name (e.g. sink
and passthrough
).
Fragment Components
A Fragment Component has no executable code, but instead just specifies some rigging. It is a "fragment" of a complete Rig.
It allows you to bundle multiple Components together into a single Component.
Within the rigging, a Fragment Component input comes from an implicit Component with the Handle input
,
and the output of the Fragment
will be the output of the Component with the Handle output
.
The slipwayhq.echarts
Component takes an
ECharts definition as an input and returns
a rendered chart as an output. However internally it is a Fragment Component which rigs together two other Components:
-
First it passes the ECharts definition to the
slipwayhq.echarts__svg
Component. This is a Javascript Component which uses the ECharts Javascript library to generate an SVG. -
Next it passes the output of that Component to the input of the
slipwayhq.svg
Component. This is a Rust WASM Component which takes an SVG as an input and renders it to an image.
Rather than the user having to rig up these two Components every time they want to render a chart, we package them up in a reusable Fragment.
Internally the rigging is defined as follows:
"rigging": {
"echarts": {
"component": "slipwayhq.echarts__svg.0.5.4",
"input": {
"width": "$$.input.width",
"height": "$$.input.height",
"chart": "$$.input.chart",
"theme": "$$?input.theme"
}
},
"output": {
"component": "slipwayhq.svg.0.6.2",
"allow": [
{ "permission": "fonts" }
],
"input": {
"width": "$$.input.width",
"height": "$$.input.height",
"svg": "$$.echarts.svg"
}
}
}
Javascript Components
A Javascript Component contains at minimum a run.js
file, which exports a run
function and sits alongside the slipway_component.json
file.
The run.js
file can import other Javascript modules from files stored within the Component.
The exported function should takes a single argument which is the Component's input JSON, and returns the Component output JSON:
export function run(input) {
return {
foo: "bar"
};
}
Javascript Components have access to a global slipway_host
object, through which they can access the Host API:
export async function run(input) {
let result = await slipway_host.fetch_text("https://icanhazip.com/");
return result.body;
}
We also polyfill some common Javascript APIs, which internally map to the host API:
export async function run(input) {
let result = await fetch("https://icanhazip.com/");
return result.text();
}
Javascript Components can also contain rigging, which will executed after the Javascript run
function has completed.
The rigging will have access to the result of the Javascript run
function through the $.input.run
query.
WASM Components
A WASM (WebAssembly) Component contains a run.wasm
file which sits alongside the slipway_component.json
file.
Slipway uses the WebAssembly Component Model for the interface between Slipway and the Component, and provides a WIT file which Components should use to generate the interface in the language of their choice.
The WIT file can be output with the following command:
slipway wit
The interface generated from this WIT file will contain both the code which enables Slipway Host to call the component, and the code which enables the component to call the Host API.
WASM Components are more complex to set up than Javascript Components, and often produce larger component file sizes for a given set of functionality.
They also currently have some limitations, such as not supporting async/await
, which can make Javascript Components more performant when many HTTP requests are required, and can be fetched in parallel.
When running Rigs through the slipway run
command, WASM Components are just-in-time (JIT) compiled each time they are run.
JIT compiling the Components has a performance overhead, often offsetting the performance gained from executing WASM in the first place.
However, when deploying as a server we can ahead-of-time (AOT) compile all the WASM Components to machine code during deployment, and then use those AOT compiled artifacts when serving Rigs:
# AOT compile the Components
slipway serve <path> aot-compile
# Run the server, using the AOT generated Components
slipway serve <path> --aot
Using these commands gives you the full performance of WASM Components when it matters most: Executing Rigs for your devices to display.
Given these tradeoffs, the recommendation is:
- Write Javascript Components for basic functionality, simple data processing, and HTTP heavy workloads.
- Write WASM Components for performance-critical data processing.
See the Guides section for more in-depth information on creating WASM Components.
WASM Components can also contain rigging, which will executed after the WASM run
function has completed.
The rigging will have access to the result of the WASM run
function through the $.input.run
query.