Compare commits

...

24 Commits
v1.0.0 ... main

Author SHA1 Message Date
Connor Smith e9ec9adf81 docs: add note about experimental status
Signed-off-by: Connor Smith <connor.smith.256@gmail.com>
2023-10-31 12:19:40 -06:00
Brooks Townsend b241df0590
Merge pull request #26 from wasmCloud/ks2211/updates
Update examples
2022-12-19 16:29:15 -05:00
Kaushik Shanadi de97386d27 update examples
Signed-off-by: Brooks Townsend <brooks@cosmonic.com>
2022-12-19 08:54:52 -05:00
Kaushik Shanadi 44c14b73cb
Merge pull request #20 from wasmCloud/snyk-upgrade-f7bfd93940c4d6a9de6e53510b435dd2
[Snyk] Upgrade axios from 0.21.3 to 0.24.0
2022-01-03 13:39:25 -05:00
Kaushik Shanadi daa3a7e099
Merge pull request #21 from wasmCloud/snyk-upgrade-5a5a87f3ce0884c20cb36e0039f28d09
[Snyk] Upgrade @msgpack/msgpack from 2.7.0 to 2.7.1
2022-01-03 13:39:10 -05:00
snyk-bot 1d33dd97f7
fix: upgrade @msgpack/msgpack from 2.7.0 to 2.7.1
Snyk has created this PR to upgrade @msgpack/msgpack from 2.7.0 to 2.7.1.

See this package in npm:
https://www.npmjs.com/package/@msgpack/msgpack

See this project in Snyk:
https://app.snyk.io/org/wasmcloud-automation/project/7349e195-0622-4c5b-add5-7f34587e7853?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-12-15 20:16:10 +00:00
snyk-bot 8d870d1546
fix: upgrade axios from 0.21.3 to 0.24.0
Snyk has created this PR to upgrade axios from 0.21.3 to 0.24.0.

See this package in npm:
https://www.npmjs.com/package/axios

See this project in Snyk:
https://app.snyk.io/org/wasmcloud-automation/project/7349e195-0622-4c5b-add5-7f34587e7853?utm_source=github&utm_medium=referral&page=upgrade-pr
2021-12-15 20:16:04 +00:00
Kaushik Shanadi 197e0ce212
Merge pull request #18 from wasmCloud/snyk-fix-702adb67744f2233deb4be91f95e8cc6
[Snyk] Security upgrade axios from 0.21.1 to 0.21.3
2021-12-14 20:15:45 -05:00
snyk-bot 2457fdf000
fix: package.json & package-lock.json to reduce vulnerabilities
The following vulnerabilities are fixed with an upgrade:
- https://snyk.io/vuln/SNYK-JS-AXIOS-1579269
2021-12-15 01:09:49 +00:00
Kaushik Shanadi 474559a0a6 update version
Signed-off-by: Kaushik Shanadi <kaushik@cosmonic.com>
2021-10-04 14:21:04 -04:00
Kaushik Shanadi 52fb0475ec
Merge pull request #16 from wasmCloud/ks2211/wapc_fix
Ks2211/wapc fix
2021-10-04 14:07:51 -04:00
Kaushik Shanadi 6d807b1892 format code
Signed-off-by: Kaushik Shanadi <kaushik@cosmonic.com>
2021-10-04 14:04:07 -04:00
Kaushik Shanadi 2f5719cca1 extend wapc-js for wasmbus, add hostcall and writer types, remove invocation callback map from startHost -- moved to launchActor call, add hostCall+writer params to actor
Signed-off-by: Kaushik Shanadi <kaushik@cosmonic.com>
2021-09-30 19:01:32 -04:00
Kaushik Shanadi e9f6d37854
Merge pull request #13 from wasmCloud/ks2211/examples
Ks2211/examples
2021-09-01 12:01:50 -04:00
ks2211 28f1232235 add jsdoc comments
Signed-off-by: ks2211 <kaushik@cosmonic.com>
2021-09-01 10:42:44 -04:00
ks2211 8ac0419b59 fix cjs build, update readme
Signed-off-by: ks2211 <kaushik@cosmonic.com>
2021-08-31 19:42:45 -04:00
ks2211 51a68841a7 add simple example, update readme for bundler instructions
Signed-off-by: ks2211 <kaushik@cosmonic.com>
2021-08-31 14:06:37 -04:00
Kaushik Shanadi 0810351fda
Merge pull request #12 from wasmCloud/ks2211/prettier
Ks2211/prettier
2021-08-31 12:06:00 -04:00
ks2211 43979140b4 webpack fixes for wasm code (can import it as a library), add invocation callbacks to launchActor (and cleanup), fix test infra docker
Signed-off-by: ks2211 <kaushik@cosmonic.com>
2021-08-31 10:51:36 -04:00
ks2211 fdbc218b04 add prettier/format code, add infra docker-compose to tests, update package.json
Signed-off-by: ks2211 <kaushik@cosmonic.com>
2021-08-25 10:54:52 -04:00
ks2211 c55c160a83 update readme for bundler instructions, upgrade to 0.2.76 bindgen, drop filename for rs files (needed for cjs)
Signed-off-by: ks2211 <kaushik@cosmonic.com>
2021-08-24 17:52:19 -04:00
ks2211 aaaf614c0f bump package version
Signed-off-by: ks2211 <kaushik@cosmonic.com>
2021-08-23 18:04:03 -04:00
ks2211 e9f626a6f2 fix wasm filename from hashing, fix gitignore
Signed-off-by: ks2211 <kaushik@cosmonic.com>
2021-08-23 18:03:27 -04:00
ks2211 7fabaadf28 cjs build fixes, generate proper library, update library bundle names, update readme
Signed-off-by: ks2211 <kaushik@cosmonic.com>
2021-08-21 09:32:36 -04:00
25 changed files with 2226 additions and 210 deletions

3
.gitignore vendored
View File

@ -1,5 +1,6 @@
node_modules/* node_modules/*
dist/* dist/*
index.html index.html
!examples/**/*index.html*
*.sh* *.sh*
wasmcloud-rs-js/pkg/ wasmcloud-rs-js/pkg/

View File

@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/prettierrc", "$schema": "https://json.schemastore.org/prettierrc",
"trailingComma": "all", "trailingComma": "none",
"tabWidth": 2, "tabWidth": 2,
"semi": true, "semi": true,
"singleQuote": true, "singleQuote": true,

113
README.md
View File

@ -1,3 +1,6 @@
> [!IMPORTANT]
> This host is **experimental** and does not implement all features or security settings. As a result, this host should not be used in production. For deploying wasmCloud to production, use the [primary host runtime](https://github.com/wasmCloud/wasmCloud/tree/main/crates/host).
# wasmCloud Host in JavaScript/Browser # wasmCloud Host in JavaScript/Browser
This is the JavaScript implementation of the wasmCloud host for the browser (NodeJS support in progress). The library runs a host inside a web browser/application that connects to a remote lattice via NATS and can run wasmCloud actors in the browser. The host will automatically listen for actor start/stop from NATS and will initialize the actor using `wapcJS`. Any invocations will be handled by the browser actor and returned to the requesting host via NATS. Users can pass callbacks to handle invocation and event data. This is the JavaScript implementation of the wasmCloud host for the browser (NodeJS support in progress). The library runs a host inside a web browser/application that connects to a remote lattice via NATS and can run wasmCloud actors in the browser. The host will automatically listen for actor start/stop from NATS and will initialize the actor using `wapcJS`. Any invocations will be handled by the browser actor and returned to the requesting host via NATS. Users can pass callbacks to handle invocation and event data.
@ -9,7 +12,7 @@ In this demonstration video we will demonstration the following:
* Load an HTTP Server capability into a wasmCloud Host running on a machine * Load an HTTP Server capability into a wasmCloud Host running on a machine
* Load an the wasmcloud-js host into a web browser * Load an the wasmcloud-js host into a web browser
* Load an 'echo' actor into the web browser * Load an 'echo' actor into the web browser
* **seemlessly** bind the actor to the capability provider through Lattice * **seamlessly** bind the actor to the capability provider through Lattice
* Access the webserver, which in turn delivers the request to the actor, processes it, and returns it to the requestion client via the capability * Access the webserver, which in turn delivers the request to the actor, processes it, and returns it to the requestion client via the capability
* Unload the actor * Unload the actor
@ -17,7 +20,6 @@ https://user-images.githubusercontent.com/1530656/130013412-b9a9daa6-fc71-424b-8
## Prerequisities ## Prerequisities
* NATS with WebSockets enabled * NATS with WebSockets enabled
@ -45,50 +47,107 @@ $ npm install @wasmcloud/wasmcloud-js
## Usage ## Usage
More examples can be found in the [examples](examples/) directory, including sample `webpack` and `esbuild` configurations
**Browser** **Browser**
```html ```html
<script src="dist/index.bundle.js"></script> <script src="https://unpkg.com/@wasmcloud/wasmcloud-js@<VERSION>/dist/wasmcloud.js"></script>
<script> <script>
(async () => { (async () => {
// start the host passing the name, registry tls enabled, a list of nats ws/wss hosts or the natsConnection object, a map of invocation callbacks, and a host heartbeat interval (default is 30 seconds) // Start the host passing the name, registry tls enabled, a list of nats ws/wss hosts or the natsConnection object, and an optional host heartbeat interval (default is 30 seconds)
const host = await wasmcloudjs.startHost("default", false, ["ws://localhost:4222"], {}, 30000); const host = await wasmcloudjs.startHost("default", false, ["ws://localhost:4222"], 30000);
// the host will automatically listen for actors start & stop messages, to manually listen for these messages // The host will automatically listen for actors start & stop messages, to manually listen for these messages the following methods are exposed
// only call these methods if your host is not listening for actor start/stop
// actor invocations are automatically returned to the host. if a user wants to handle the data, they can pass a map of callbacks using the actor ref/wasm file name as the key with a callback(data, result) function. The data contains the invocation data and the result contains the invocation result // actor invocations are automatically returned to the host. if a user wants to handle the data, they can pass a map of callbacks using the actor ref/wasm file name as the key with a callback(data, result) function. The data contains the invocation data and the result contains the invocation result
(async() => { // (async() => {
await host.listenLaunchActor( // await host.listenLaunchActor(
{ // {
"localhost:5000/echo:0.2.2": (data, result) => console.log(data.operation, result); // "localhost:5000/echo:0.2.2": (data, result) => console.log(data.operation, result);
} // }
); // );
await host.listenStopActor(); // await host.listenStopActor();
})(); // })();
// to launch an actor manually from the library from a registry // To launch an actor manually from the library from a registry, optionally a callback can be passed to handle the invocation results. In addition, a hostCall callback and writer can be passed.
await host.launchActor("registry.com/actor:0.1.1") // The hostCallback format is as follows:
// to launch an actrom manually from local disk (note the .wasm is required) // ```
// (binding, namespace, operation, payload) => {
// return Uint8Array(payload);
// })
// ```
await host.launchActor("registry.com/actor:0.1.1", (data) => { /* handle data */})
// Launch an actor with the hostCallback
await host.launchActor("registry.com/actor:0.1.1", (data) => { /* handle data */}, (binding, namespace, operation, payload) => {
// decode payload via messagepack
// const decoded = decode(payload);
return new Uint8Array(payload);
})
// To launch an actrom manually from local disk (note the .wasm is required)
await host.launchActor("./actor.wasm"); await host.launchActor("./actor.wasm");
// to listen for events, you can call the subscribeToEvents and pass an optional callback to handle the event data // To listen for events, you can call the subscribeToEvents and pass an optional callback to handle the event data
await host.subscribeToEvents((eventData) => console.log(eventData, eventData.source)); await host.subscribeToEvents((eventData) => console.log(eventData, eventData.source));
// to unsubscribe, call the unsubscribeEvents // To unsubscribe, call the unsubscribeEvents
await host.unsubscribeEvents(); await host.unsubscribeEvents();
// to start & stop the heartbeat events // To start & stop the heartbeat events
await host.startHeartbeat(heartbeatInterval?); await host.startHeartbeat();
await host.stopHeartbeat(); await host.stopHeartbeat();
// the host will automatically connect to nats on start. to connect/reconnect to nats // The host will automatically connect to nats on start. to connect/reconnect to nats
await host.connectNATS(["ws://locahost:4222/"], {}); await host.connectNATS();
// to close/drain all connections from nats, call the disconnectNATS() method // To close/drain all connections from nats, call the disconnectNATS() method
await host.disconnectNATS(); await host.disconnectNATS();
// stop the host // Stop the host
await host.stopHost(); await host.stopHost();
// restart the host (this only needs to be called if the host is stopped, it is automatically called on the constructor) // Restart the host (this only needs to be called if the host is stopped, it is automatically called on the constructor)
await host.startHost(); await host.startHost();
})(); })();
</script> </script>
``` ```
**With a bundler**
There are some caveats to using with a bundler:
* The module contains `.wasm` files that need to be present alongside the final build output. Using `webpack-copy-plugin` (or `fs.copyFile` with other bundlers) can solve this issue.
* If using with `create-react-app`, the webpack config will need to be ejected via `npm run eject` OR an npm library like `react-app-rewired` can handle the config injection.
```javascript
// as esm -- this will grant you access to the types/params
import { startHost } from '@wasmcloud/wasmcloud-js';
// as cjs
// const wasmcloudjs = require('@wasmcloud/wasmcloud-js);
async function cjsHost() {
const host = await wasmcloudjs.startHost('default', false, ['ws://localhost:4222'])
console.log(host);
}
async function esmHost() {
const host = await startHost('default', false, ['ws://localhost:4222'])
console.log(host);
}
cjsHost();
esmHost();
```
```javascript
// webpack config, add this to the plugin section
plugins: [
new CopyPlugin({
patterns: [
{
from: 'node_modules/@wasmcloud/wasmcloud-js/dist/wasmcloud-rs-js/pkg/*.wasm',
to: '[name].wasm'
}
]
}),
]
```
**Node** **Node**
In progress--wasm-pack compile issues with nodeJS. *IN PROGRESS* - NodeJS does not support WebSockets natively (required by nats.ws)
## Contributing ## Contributing

50
examples/simple/README.md Normal file
View File

@ -0,0 +1,50 @@
# wasmcloud-js Examples
This directory contains examples of using the `wasmcloud-js` library with sample `webpack` and `esbuild` configurations.
## Prerequisities
* NATS with WebSockets enabled
* There is sample infra via docker in the `test/infra` directory of this repo, `cd test/infra && docker-compose up`
* wasmCloud lattice (OTP Version)
* (OPTIONAL) Docker Registry with CORS configured
* If launching actors from remote registries in the browser host, CORS must be configured on the registry server
* NodeJS, NPM, npx
## Build
```sh
$ npm install # this will run and build the rust deps
$ npm install webpack esbuild copy-webpack-plugin fs
$ #### if you want to use esbuild, follow these next 2 steps ####
$ node esbuild.js # this produces the esbuild version
$ mv out-esbuild.js out.js # rename the esbuild version
$ #### if you want to use webpack, follow the steps below ####
$ npx webpack --config=example-webpack.config.js # this produces the webpack output
$ mv out-webpack.js out.js #rename the webpack version to out.js
```
## Usage
1. Build the code with esbuild or webpack
2. Rename the output file to `out.js`
3. Start a web server inside this directory (e.g `python3 -m http.server` or `npx serve`)
3. Navigate to a browser `localhost:<PORT>`
4. Open the developer console to view the host output
5. In the dev tools run `host` and you will get the full host object and methods
6. Start an actor with `host.launchActor('registry:5000/image', (data) => console.log(data))`. Echo actor is recommended (the 2nd parameter here is an invocation callback to handle the data from an invocation)
7. Link the actor with a provider running in Wasmcloud (eg `httpserver`)
8. Run a `curl localhost:port/echo` to see the response in the console (based off the invocation callback).

View File

@ -0,0 +1,18 @@
// Use this path in projects using the node import
let defaultWasmFileLocation = './node_modules/@wasmcloud/wasmcloud-js/dist/wasmcloud-rs-js/pkgwasmcloud.wasm'
let wasmFileLocationForLocal = '../../dist/wasmcloud-rs-js/pkg/wasmcloud.wasm'
let copyPlugin = {
name: 'copy',
setup(build) {
require('fs').copyFile(wasmFileLocationForLocal, `${process.cwd()}/wasmcloud.wasm`, (err) => {
if (err) throw err;
});
}
}
require('esbuild').build({
entryPoints: ['main.js'],
bundle: true,
outfile: 'out-esbuild.js',
plugins: [copyPlugin]
}).catch(() => process.exit(1))

View File

@ -0,0 +1,41 @@
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin')
module.exports = {
stats: { assets: false, modules: false },
entry: './main.js',
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
// this is needed to copy the wasm file used by the js code to initiate a host key/extract the token from a signed actor
// this SHOULD go away once the upstream webpack build issues are resolved (webpack will automatically pick up the webpack file without needing a copy)
plugins: [
new CopyPlugin({
patterns: [
{
// the node_module path should be referenced in projects using the node import
// from: 'node_modules/@wasmcloud/wasmcloud-js/dist/*.wasm',
from: '../../dist/wasmcloud-rs-js/pkg/*.wasm',
to: '[name].wasm'
}
]
})
],
mode: 'production',
resolve: {
extensions: ['.tsx', '.ts', '.js', '.wasm']
},
experiments: {
asyncWebAssembly: true
},
output: {
filename: 'out-webpack.js',
path: path.resolve(__dirname, '')
}
}

View File

@ -0,0 +1,22 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World - wasmcloudjs</title>
<!-- <script src="https://unpkg.com/@wasmcloud/wasmcloud-js@1.0.4/dist/wasmcloud.js"></script> -->
<script src="./out.js"></script>
<!-- this uses the npm library-->
<!-- <script src="./out-webpack.js"></script> -->
<!-- <script src="./out-esbuild.js"></script> -->
<script>
// (async() => {
// console.log("USING THE BROWSER BUNDLE")
// const host = await wasmcloudjs.startHost("default", false, ["ws://localhost:4222"])
// console.log(host);
// })()
</script>
</head>
<body>
</body>
</html>

12
examples/simple/main.js Normal file
View File

@ -0,0 +1,12 @@
import { startHost } from '../../dist/src'
// Importing inside of a project
// import { startHost } from '@wasmcloud/wasmcloud-js';
// const { startHost } = require('@wasmcloud/wasmcloud-js');
(async () => {
console.log('USING A JS BUNDLER')
const host = await startHost('default', false, ['ws://localhost:6222'])
window.host = host;
console.log(host);
})()

1544
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,32 @@
{ {
"name": "@wasmcloud/wasmcloud-js", "name": "@wasmcloud/wasmcloud-js",
"version": "1.0.0", "version": "1.0.6",
"description": "wasmcloud host in JavaScript/Browser", "description": "wasmcloud host in JavaScript/Browser",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
"files": [ "files": [
"dist", "dist",
"src", "src",
"wasmcloud-rs-js", "README.md",
"README.md" "wasmcloud-rs-js"
], ],
"scripts": { "scripts": {
"build": "npm run clean && npm run build:browser && npm run build:cjs", "build": "npm run clean && npm run build:browser && npm run build:cjs",
"build:browser": "webpack --mode=production", "build:browser": "webpack",
"build:cjs": "tsc --declaration", "build:cjs": "tsc --declaration && webpack --env target=cjs",
"build:wasm": "cd wasmcloud-rs-js && wasm-pack build", "build:wasm": "cd wasmcloud-rs-js && wasm-pack build",
"clean": "rm -rf ./dist/ && rm -rf ./wasmcloud-rs-js/pkg/", "clean": "rm -rf ./dist/ && rm -rf ./wasmcloud-rs-js/pkg/",
"lint": "eslint --ext .ts src test", "lint": "eslint --ext .ts src test",
"format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
"test": "mocha", "test": "mocha --require ts-node/register",
"watch": "npm run clean && tsc -w --declrataion", "watch": "npm run clean && tsc -w --declrataion",
"prepare": "npm run build" "prepare": "npm run build"
}, },
"prettier": ".prettierrc.json", "prettier": "./.prettierrc.json",
"keywords": [ "keywords": [
"wasmcloud", "wasmcloud",
"wasmcloud-host", "wasmcloud-host",
"wasmcloud-js",
"wasm" "wasm"
], ],
"eslintConfig": { "eslintConfig": {
@ -45,24 +46,26 @@
"extension": [ "extension": [
"ts" "ts"
], ],
"spec": "test/**/*.test.ts", "spec": "test/**/*.test.ts"
"require": "ts-node/register"
}, },
"author": "ks2211 <kaushik@cosmonic.com>", "author": "ks2211 <kaushik@cosmonic.com>",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@types/chai": "^4.2.21", "@types/chai": "^4.2.21",
"@types/chai-as-promised": "^7.1.4", "@types/chai-as-promised": "^7.1.4",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.29.2", "@typescript-eslint/parser": "^4.29.2",
"@wasm-tool/wasm-pack-plugin": "^1.5.0", "@wasm-tool/wasm-pack-plugin": "^1.5.0",
"babel-loader": "^8.2.2",
"chai": "^4.3.4", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"esbuild": "^0.12.20",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"mocha": "^9.0.3", "mocha": "^9.0.3",
"path": "^0.12.7", "path": "^0.12.7",
"prettier": "^2.3.2",
"ts-loader": "^9.2.5", "ts-loader": "^9.2.5",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"typescript": "^4.3.5", "typescript": "^4.3.5",
@ -70,9 +73,9 @@
"webpack-cli": "^4.8.0" "webpack-cli": "^4.8.0"
}, },
"dependencies": { "dependencies": {
"@msgpack/msgpack": "^2.7.0", "@msgpack/msgpack": "^2.7.1",
"@wapc/host": "0.0.2", "@wapc/host": "0.0.2",
"axios": "^0.21.1", "axios": "^0.24.0",
"nats.ws": "^1.2.0" "nats.ws": "^1.2.0"
} }
} }

View File

@ -1,24 +1,42 @@
import { encode, decode } from '@msgpack/msgpack'; import { encode, decode } from '@msgpack/msgpack';
import { instantiate, WapcHost } from '@wapc/host';
import { NatsConnection, Subscription } from 'nats.ws'; import { NatsConnection, Subscription } from 'nats.ws';
import { instantiate, Wasmbus } from './wasmbus';
import { createEventMessage, EventType } from './events'; import { createEventMessage, EventType } from './events';
import { import {
ActorClaims, ActorClaimsMessage, ActorStartedMessage, ActorHealthCheckPassMessage, ActorClaims,
ActorClaimsMessage,
ActorStartedMessage,
ActorHealthCheckPassMessage,
InvocationMessage, InvocationMessage,
StopActorMessage StopActorMessage,
HostCall,
Writer
} from './types'; } from './types';
import { jsonEncode, parseJwt, uuidv4 } from './util'; import { jsonEncode, parseJwt, uuidv4 } from './util';
/**
* Actor holds the actor wasm module
*/
export class Actor { export class Actor {
claims: ActorClaims; claims: ActorClaims;
key: string; key: string;
module!: WapcHost; module!: Wasmbus;
hostKey: string; hostKey: string;
hostName: string; hostName: string;
wasm: any; wasm: any;
invocationCallback?: Function;
hostCall?: HostCall;
writer?: Writer;
constructor(hostName: string = 'default', hostKey: string, wasm: any) { constructor(
hostName: string = 'default',
hostKey: string,
wasm: any,
invocationCallback?: Function,
hostCall?: HostCall,
writer?: Writer
) {
this.key = ''; this.key = '';
this.hostName = hostName; this.hostName = hostName;
this.hostKey = hostKey; this.hostKey = hostKey;
@ -37,8 +55,16 @@ export class Actor {
} }
}; };
this.wasm = wasm; this.wasm = wasm;
this.invocationCallback = invocationCallback;
this.hostCall = hostCall;
this.writer = writer;
} }
/**
* startActor takes an actor wasm uint8array, extracts the jwt, validates the jwt, and uses wapcJS to instantiate the module
*
* @param {Uint8Array} actorBuffer - the wasm actor module as uint8array
*/
async startActor(actorBuffer: Uint8Array) { async startActor(actorBuffer: Uint8Array) {
const token: string = await this.wasm.extract_jwt(actorBuffer); const token: string = await this.wasm.extract_jwt(actorBuffer);
const valid: boolean = await this.wasm.validate_jwt(token); const valid: boolean = await this.wasm.validate_jwt(token);
@ -47,17 +73,27 @@ export class Actor {
} }
this.claims = parseJwt(token); this.claims = parseJwt(token);
this.key = this.claims.sub; this.key = this.claims.sub;
this.module = await instantiate(actorBuffer); this.module = await instantiate(actorBuffer, this.hostCall, this.writer);
} }
/**
* stopActor publishes the stop_actor message
*
* @param {NatsConnection} natsConn - the nats connection object
*/
async stopActor(natsConn: NatsConnection) { async stopActor(natsConn: NatsConnection) {
const actorToStop: StopActorMessage = { const actorToStop: StopActorMessage = {
host_id: this.hostKey, host_id: this.hostKey,
actor_ref: this.key actor_ref: this.key
} };
natsConn.publish(`wasmbus.ctl.${this.hostName}.cmd.${this.hostKey}.sa`, jsonEncode(actorToStop)) natsConn.publish(`wasmbus.ctl.${this.hostName}.cmd.${this.hostKey}.sa`, jsonEncode(actorToStop));
} }
/**
* publishActorStarted publishes the claims, the actor_started, and health_check_pass messages
*
* @param {NatsConnection} natsConn - the natsConnection object
*/
async publishActorStarted(natsConn: NatsConnection) { async publishActorStarted(natsConn: NatsConnection) {
// publish claims // publish claims
const claims: ActorClaimsMessage = { const claims: ActorClaimsMessage = {
@ -69,59 +105,85 @@ export class Actor {
sub: this.claims.sub, sub: this.claims.sub,
tags: '', tags: '',
version: this.claims.wascap.ver version: this.claims.wascap.ver
} };
natsConn.publish(`lc.${this.hostName}.claims.${this.key}`, jsonEncode(claims)) natsConn.publish(`lc.${this.hostName}.claims.${this.key}`, jsonEncode(claims));
// publish actor_started // publish actor_started
const actorStarted: ActorStartedMessage = { const actorStarted: ActorStartedMessage = {
api_version: 0, api_version: 0,
instance_id: uuidv4(), instance_id: uuidv4(),
public_key: this.key public_key: this.key
} };
natsConn.publish(`wasmbus.evt.${this.hostName}`, jsonEncode(createEventMessage(this.hostKey, EventType.ActorStarted, actorStarted))); natsConn.publish(
`wasmbus.evt.${this.hostName}`,
jsonEncode(createEventMessage(this.hostKey, EventType.ActorStarted, actorStarted))
);
// publish actor health_check // publish actor health_check
const actorHealthCheck: ActorHealthCheckPassMessage = { const actorHealthCheck: ActorHealthCheckPassMessage = {
instance_id: uuidv4(), instance_id: uuidv4(),
public_key: this.key public_key: this.key
} };
natsConn.publish(`wasmbus.evt.${this.hostName}`, jsonEncode(createEventMessage(this.hostKey, EventType.HealthCheckPass, actorHealthCheck))); natsConn.publish(
`wasmbus.evt.${this.hostName}`,
jsonEncode(createEventMessage(this.hostKey, EventType.HealthCheckPass, actorHealthCheck))
);
} }
async subscribeInvocations(natsConn: NatsConnection, invocationCallback?: Function) { /**
* subscribeInvocations does a subscribe on nats for invocations
*
* @param {NatsConnection} natsConn the nats connection object
*/
async subscribeInvocations(natsConn: NatsConnection) {
// subscribe to topic, wait for invokes, invoke the host, if callback set, send message // subscribe to topic, wait for invokes, invoke the host, if callback set, send message
const invocationsTopic: Subscription = natsConn.subscribe(`wasmbus.rpc.${this.hostName}.${this.key}`); const invocationsTopic: Subscription = natsConn.subscribe(`wasmbus.rpc.${this.hostName}.${this.key}`);
for await (const invocationMessage of invocationsTopic) { for await (const invocationMessage of invocationsTopic) {
const invocationData = decode(invocationMessage.data); const invocationData = decode(invocationMessage.data);
const invocation: InvocationMessage = (invocationData as InvocationMessage) const invocation: InvocationMessage = invocationData as InvocationMessage;
const invocationResult: Uint8Array = await this.module.invoke(invocation.operation, invocation.msg); const invocationResult: Uint8Array = await this.module.invoke(invocation.operation, invocation.msg);
invocationMessage.respond(encode({ invocationMessage.respond(
invocation_id: (invocationData as any).id, encode({
instance_id: uuidv4(), invocation_id: (invocationData as any).id,
msg: invocationResult instance_id: uuidv4(),
})); msg: invocationResult
if (invocationCallback) { })
invocationCallback(invocationResult); );
if (this.invocationCallback) {
this.invocationCallback(invocationResult);
} }
} }
throw new Error('actor.inovcation subscription closed'); throw new Error('actor.inovcation subscription closed');
} }
} }
/**
export async function newActor(hostName: string, hostKey: string, * startActor initializes an actor and listens for invocation messages
*
* @param {string} hostName - the name of the host
* @param {string} hostKey - the publickey of the host
* @param {Uint8Array} actorModule - the wasm module of the actor
* @param {NatsConnection} natsConn - the nats connection object
* @param {any} wasm - the rust wasm module
* @param {Function} invocationCallback - an optional function to call when the invocation is successful
* @param {HostCall} hostCall - the hostCallback
* @param {Writer} writer - the hostCallback writer
* @returns {Actor}
*/
export async function startActor(
hostName: string,
hostKey: string,
actorModule: Uint8Array, actorModule: Uint8Array,
natsConn: NatsConnection, natsConn: NatsConnection,
wasm: any, wasm: any,
invocationCallback?: Function invocationCallback?: Function,
hostCall?: HostCall,
writer?: Writer
): Promise<Actor> { ): Promise<Actor> {
const actor: Actor = new Actor(hostName, hostKey, wasm); const actor: Actor = new Actor(hostName, hostKey, wasm, invocationCallback, hostCall, writer);
await actor.startActor(actorModule); await actor.startActor(actorModule);
await actor.publishActorStarted(natsConn); await actor.publishActorStarted(natsConn);
Promise.all([ Promise.all([actor.subscribeInvocations(natsConn)]).catch(err => {
actor.subscribeInvocations(natsConn, invocationCallback)
]).catch((err) => {
throw err; throw err;
}); });
return actor; return actor;
} }

View File

@ -1,10 +1,10 @@
import { uuidv4 } from './util' import { uuidv4 } from './util';
export enum EventType { export enum EventType {
HeartBeat = 'com.wasmcloud.lattice.host_heartbeat', HeartBeat = 'com.wasmcloud.lattice.host_heartbeat',
ActorStarted = 'com.wasmcloud.lattice.actor_started', ActorStarted = 'com.wasmcloud.lattice.actor_started',
ActorStopped = 'com.wasmcloud.lattice.actor_stopped', ActorStopped = 'com.wasmcloud.lattice.actor_stopped',
HealthCheckPass = 'com.wasmcloud.lattice.health_check_passed', HealthCheckPass = 'com.wasmcloud.lattice.health_check_passed'
} }
export type EventData = { export type EventData = {
@ -15,8 +15,16 @@ export type EventData = {
time: string; time: string;
type: EventType; type: EventType;
data: any; data: any;
} };
/**
* createEventMessage is a helper function to create a message for "wasmbus.evt.{host}"
*
* @param {string} hostKey - the host public key
* @param {EventType} eventType - the event type using the EventType enum
* @param {any} data - the json data object
* @returns {EventData}
*/
export function createEventMessage(hostKey: string, eventType: EventType, data: any): EventData { export function createEventMessage(hostKey: string, eventType: EventType, data: any): EventData {
return { return {
data: data, data: data,
@ -26,5 +34,5 @@ export function createEventMessage(hostKey: string, eventType: EventType, data:
specversion: '1.0', specversion: '1.0',
time: new Date().toISOString(), time: new Date().toISOString(),
type: eventType type: eventType
} };
} }

View File

@ -4,7 +4,7 @@ export type ImageDigest = {
name: string; name: string;
digest: string; digest: string;
registry: string; registry: string;
} };
type FetchActorDigestResponse = { type FetchActorDigestResponse = {
schemaVersion: number; schemaVersion: number;
@ -19,7 +19,7 @@ type FetchActorDigestResponse = {
layers: Array<{ layers: Array<{
annotations: { annotations: {
['org.opencontainers.image.title']: string; ['org.opencontainers.image.title']: string;
} };
digest: string; digest: string;
mediaType: string; mediaType: string;
size: number; size: number;
@ -28,18 +28,27 @@ type FetchActorDigestResponse = {
annotations: any; annotations: any;
}; };
/**
* fetchActorDigest fetches the actor digest from a registry (sha)
*
* @param {string} actorRef - the actor url e.g host:port/image:version
* @param {boolean} withTLS - whether or not the registry uses tls
* @returns {ImageDigest}
*/
export async function fetchActorDigest(actorRef: string, withTLS?: boolean): Promise<ImageDigest> { export async function fetchActorDigest(actorRef: string, withTLS?: boolean): Promise<ImageDigest> {
const image: Array<string> = actorRef.split('/'); const image: Array<string> = actorRef.split('/');
const registry: string = image[0]; const registry: string = image[0];
const [name, version] = image[1].split(':'); const [name, version] = image[1].split(':');
const response: AxiosResponse = await axios.get( const response: AxiosResponse = await axios
`${withTLS ? 'https://' : 'http://'}${registry}/v2/${name}/manifests/${version}`, .get(`${withTLS ? 'https://' : 'http://'}${registry}/v2/${name}/manifests/${version}`, {
{
headers: { headers: {
'Accept': 'application/vnd.oci.image.manifest.v1+json' Accept: 'application/vnd.oci.image.manifest.v1+json'
} }
}).catch((err) => { throw err }); })
.catch(err => {
throw err;
});
const layers: FetchActorDigestResponse = response.data; const layers: FetchActorDigestResponse = response.data;
if (layers.layers.length === 0) { if (layers.layers.length === 0) {
@ -50,17 +59,23 @@ export async function fetchActorDigest(actorRef: string, withTLS?: boolean): Pro
name, name,
digest: layers.layers[0].digest, digest: layers.layers[0].digest,
registry registry
} };
} }
/**
* fetchActor fetches an actor from either the local disk or a registry and returns it as uint8array
*
* @param {string} url - the url of the actor module
* @returns {Uint8Array}
*/
export async function fetchActor(url: string): Promise<Uint8Array> { export async function fetchActor(url: string): Promise<Uint8Array> {
const response: AxiosResponse = await axios.get( const response: AxiosResponse = await axios
url, .get(url, {
{
responseType: 'arraybuffer' responseType: 'arraybuffer'
} })
).catch((err) => { throw err }); .catch(err => {
throw err;
});
return new Uint8Array(response.data); return new Uint8Array(response.data);
} }

View File

@ -1,11 +1,7 @@
import { encode } from '@msgpack/msgpack'; import { encode } from '@msgpack/msgpack';
import { import { connect, ConnectionOptions, NatsConnection, Subscription } from 'nats.ws';
connect, ConnectionOptions,
NatsConnection,
Subscription
} from 'nats.ws';
import { Actor, newActor } from './actor'; import { Actor, startActor } from './actor';
import { createEventMessage, EventType } from './events'; import { createEventMessage, EventType } from './events';
import { fetchActor, fetchActorDigest, ImageDigest } from './fetch'; import { fetchActor, fetchActorDigest, ImageDigest } from './fetch';
import { import {
@ -14,19 +10,23 @@ import {
HeartbeatMessage, HeartbeatMessage,
InvocationCallbacks, InvocationCallbacks,
LaunchActorMessage, LaunchActorMessage,
StopActorMessage StopActorMessage,
HostCall,
Writer
} from './types'; } from './types';
import { jsonDecode, jsonEncode, uuidv4 } from './util'; import { jsonDecode, jsonEncode, uuidv4 } from './util';
const HOST_HEARTBEAT_INTERVAL: number = 30000; const HOST_HEARTBEAT_INTERVAL: number = 30000;
/**
* Host holds the js/browser host
*/
export class Host { export class Host {
name: string; name: string;
key: string; key: string;
seed: string; seed: string;
heartbeatInterval: number; heartbeatInterval: number;
heartbeatIntervalId: any; heartbeatIntervalId: any;
invocationCallbacks?: InvocationCallbacks;
withRegistryTLS: boolean; withRegistryTLS: boolean;
actors: { actors: {
[key: string]: { [key: string]: {
@ -38,14 +38,20 @@ export class Host {
natsConn!: NatsConnection; natsConn!: NatsConnection;
eventsSubscription!: Subscription | null; eventsSubscription!: Subscription | null;
wasm: any; wasm: any;
invocationCallbacks?: InvocationCallbacks;
hostCalls?: {
[key: string]: HostCall;
};
writers?: {
[key: string]: Writer;
};
constructor(
constructor(name: string = 'default', name: string = 'default',
withRegistryTLS: boolean, withRegistryTLS: boolean,
heartbeatInterval: number, heartbeatInterval: number,
natsConnOpts: Array<string> | ConnectionOptions, natsConnOpts: Array<string> | ConnectionOptions,
wasm: any, wasm: any
invocationCallbacks?: InvocationCallbacks
) { ) {
const hostKey = new wasm.HostKey(); const hostKey = new wasm.HostKey();
this.name = name; this.name = name;
@ -56,46 +62,68 @@ export class Host {
this.wasm = wasm; this.wasm = wasm;
this.heartbeatInterval = heartbeatInterval; this.heartbeatInterval = heartbeatInterval;
this.natsConnOpts = natsConnOpts; this.natsConnOpts = natsConnOpts;
this.invocationCallbacks = invocationCallbacks; this.invocationCallbacks = {};
this.hostCalls = {};
this.writers = {};
} }
/**
* connectNATS connects to nats using either the array of servers or the connection options object
*/
async connectNATS() { async connectNATS() {
const opts: ConnectionOptions = (Array.isArray(this.natsConnOpts)) ? { const opts: ConnectionOptions = Array.isArray(this.natsConnOpts)
servers: this.natsConnOpts ? {
} : this.natsConnOpts; servers: this.natsConnOpts
}
: this.natsConnOpts;
this.natsConn = await connect(opts); this.natsConn = await connect(opts);
} }
/**
* disconnectNATS disconnects from nats
*/
async disconnectNATS() { async disconnectNATS() {
this.natsConn.close(); this.natsConn.close();
} }
/**
* startHeartbeat starts a heartbeat publish message every X seconds based on the interval
*/
async startHeartbeat() { async startHeartbeat() {
this.heartbeatIntervalId; this.heartbeatIntervalId;
const heartbeat: HeartbeatMessage = { const heartbeat: HeartbeatMessage = {
actors: [], actors: [],
providers: [] providers: []
} };
for (const actor in this.actors) { for (const actor in this.actors) {
heartbeat.actors.push({ heartbeat.actors.push({
actor: actor, actor: actor,
instances: 1 instances: 1
}) });
} }
this.heartbeatIntervalId = await setInterval(() => { this.heartbeatIntervalId = await setInterval(() => {
this.natsConn.publish(`wasmbus.evt.${this.name}`, this.natsConn.publish(
`wasmbus.evt.${this.name}`,
jsonEncode(createEventMessage(this.key, EventType.HeartBeat, heartbeat)) jsonEncode(createEventMessage(this.key, EventType.HeartBeat, heartbeat))
); );
}, this.heartbeatInterval) }, this.heartbeatInterval);
} }
/**
* stopHeartbeat clears the heartbeat interval
*/
async stopHeartbeat() { async stopHeartbeat() {
clearInterval(this.heartbeatIntervalId); clearInterval(this.heartbeatIntervalId);
this.heartbeatIntervalId = null; this.heartbeatIntervalId = null;
} }
/**
* subscribeToEvents subscribes to the events on the host
*
* @param eventCallback - an optional callback(data) to handle the event message
*/
async subscribeToEvents(eventCallback?: Function) { async subscribeToEvents(eventCallback?: Function) {
this.eventsSubscription = this.natsConn.subscribe(`wasmbus.evt.${this.name}`) this.eventsSubscription = this.natsConn.subscribe(`wasmbus.evt.${this.name}`);
for await (const event of this.eventsSubscription) { for await (const event of this.eventsSubscription) {
const eventData = jsonDecode(event.data); const eventData = jsonDecode(event.data);
if (eventCallback) { if (eventCallback) {
@ -105,28 +133,55 @@ export class Host {
throw new Error('evt subscription was closed'); throw new Error('evt subscription was closed');
} }
/**
* unsubscribeEvents unsubscribes and removes the events subscription
*/
async unsubscribeEvents() { async unsubscribeEvents() {
this.eventsSubscription?.unsubscribe(); this.eventsSubscription?.unsubscribe();
this.eventsSubscription = null; this.eventsSubscription = null;
} }
/**
async launchActor(actorRef: string) { * launchActor launches an actor via the launch actor message
*
* @param actorRef - the actor to start
* @param invocationCallback - an optional callback(data) to handle the invocation
* @param hostCall - the hostCallback
* @param writer - writer for the hostCallback, can be undefined
*/
async launchActor(actorRef: string, invocationCallback?: Function, hostCall?: HostCall, writer?: Writer) {
const actor: LaunchActorMessage = { const actor: LaunchActorMessage = {
actor_ref: actorRef, actor_ref: actorRef,
host_id: this.key host_id: this.key
} };
this.natsConn.publish(`wasmbus.ctl.${this.name}.cmd.${this.key}.la`, jsonEncode(actor)); this.natsConn.publish(`wasmbus.ctl.${this.name}.cmd.${this.key}.la`, jsonEncode(actor));
if (invocationCallback) {
this.invocationCallbacks![actorRef] = invocationCallback;
}
if (hostCall) {
this.hostCalls![actorRef] = hostCall;
}
if (writer) {
this.writers![actorRef] = writer;
}
} }
/**
* stopActor stops an actor by publishing the sa message
*
* @param {string} actorRef - the actor to stop
*/
async stopActor(actorRef: string) { async stopActor(actorRef: string) {
const actorToStop: StopActorMessage = { const actorToStop: StopActorMessage = {
host_id: this.key, host_id: this.key,
actor_ref: actorRef actor_ref: actorRef
} };
this.natsConn.publish(`wasmbus.ctl.${this.name}.cmd.${this.key}.sa`, jsonEncode(actorToStop)) this.natsConn.publish(`wasmbus.ctl.${this.name}.cmd.${this.key}.sa`, jsonEncode(actorToStop));
} }
/**
* listenLaunchActor listens for start actor message and will fetch the actor (either from disk or registry) and initialize the actor
*/
async listenLaunchActor() { async listenLaunchActor() {
// subscribe to the .la topic `wasmbus.ctl.${this.name}.cmd.${this.key}.la` // subscribe to the .la topic `wasmbus.ctl.${this.name}.cmd.${this.key}.la`
// decode the data // decode the data
@ -142,16 +197,22 @@ export class Host {
let url: string; let url: string;
if (usingRegistry) { if (usingRegistry) {
const actorDigest: ImageDigest = await fetchActorDigest(actorRef); const actorDigest: ImageDigest = await fetchActorDigest(actorRef);
url = `${this.withRegistryTLS ? 'https://' : 'http://'}${actorDigest.registry}/v2/${actorDigest.name}/blobs/${actorDigest.digest}` url = `${this.withRegistryTLS ? 'https://' : 'http://'}${actorDigest.registry}/v2/${actorDigest.name}/blobs/${
actorDigest.digest
}`;
} else { } else {
url = actorRef; url = actorRef;
} }
const actorModule: Uint8Array = await fetchActor(url); const actorModule: Uint8Array = await fetchActor(url);
const actor: Actor = await newActor(this.name, this.key, const actor: Actor = await startActor(
this.name,
this.key,
actorModule, actorModule,
this.natsConn, this.natsConn,
this.wasm, this.wasm,
this.invocationCallbacks?.[actorRef] this.invocationCallbacks?.[actorRef],
this.hostCalls?.[actorRef],
this.writers?.[actorRef]
); );
if (this.actors[actorRef]) { if (this.actors[actorRef]) {
@ -160,34 +221,49 @@ export class Host {
this.actors[actorRef] = { this.actors[actorRef] = {
count: 1, count: 1,
actor: actor actor: actor
} };
} }
} catch (err) { } catch (err) {
// TODO: error handling // TODO: error handling
console.log('error', err); console.log('error', err);
} }
} }
throw new Error('la.subscription was closed') throw new Error('la.subscription was closed');
} }
/**
* listenStopActor listens for the actor stopped message and will tear down the actor on message receive
*/
async listenStopActor() { async listenStopActor() {
// listen for stop actor message, decode the data // listen for stop actor message, decode the data
// publish actor_stopped to the lattice // publish actor_stopped to the lattice
// delete the actor from the host // delete the actor from the host and remove the invocation callback
const actorsTopic: Subscription = this.natsConn.subscribe(`wasmbus.ctl.${this.name}.cmd.${this.key}.sa`); const actorsTopic: Subscription = this.natsConn.subscribe(`wasmbus.ctl.${this.name}.cmd.${this.key}.sa`);
for await (const actorMessage of actorsTopic) { for await (const actorMessage of actorsTopic) {
const actorData = jsonDecode(actorMessage.data); const actorData = jsonDecode(actorMessage.data);
const actorStop: ActorStoppedMessage = { const actorStop: ActorStoppedMessage = {
instance_id: uuidv4(), instance_id: uuidv4(),
public_key: this.actors[(actorData as StopActorMessage).actor_ref].actor.key public_key: this.actors[(actorData as StopActorMessage).actor_ref].actor.key
} };
this.natsConn.publish(`wasmbus.evt.${this.name}`, this.natsConn.publish(
jsonEncode(createEventMessage(this.key, EventType.ActorStopped, actorStop))) `wasmbus.evt.${this.name}`,
jsonEncode(createEventMessage(this.key, EventType.ActorStopped, actorStop))
);
delete this.actors[(actorData as StopActorMessage).actor_ref]; delete this.actors[(actorData as StopActorMessage).actor_ref];
delete this.invocationCallbacks![(actorData as StopActorMessage).actor_ref];
} }
throw new Error('sa.subscription was closed') throw new Error('sa.subscription was closed');
} }
/**
* createLinkDefinition creates a link definition between an actor and a provider (unused)
*
* @param {string} actorKey - the actor key
* @param {string} providerKey - the provider public key
* @param {string} linkName - the name of the link
* @param {string} contractId - the contract id of the linkdef
* @param {any} values - list of key/value pairs to pass for the linkdef
*/
async createLinkDefinition(actorKey: string, providerKey: string, linkName: string, contractId: string, values: any) { async createLinkDefinition(actorKey: string, providerKey: string, linkName: string, contractId: string, values: any) {
const linkDefinition: CreateLinkDefMessage = { const linkDefinition: CreateLinkDefMessage = {
actor_id: actorKey, actor_id: actorKey,
@ -195,21 +271,23 @@ export class Host {
link_name: linkName, link_name: linkName,
contract_id: contractId, contract_id: contractId,
values: values values: values
} };
this.natsConn.publish(`wasmbus.rpc.${this.name}.${providerKey}.${linkName}.linkdefs.put`, encode(linkDefinition)) this.natsConn.publish(`wasmbus.rpc.${this.name}.${providerKey}.${linkName}.linkdefs.put`, encode(linkDefinition));
} }
/**
* startHost connects to nats, starts the heartbeat, listens for actors start/stop
*/
async startHost() { async startHost() {
await this.connectNATS(); await this.connectNATS();
Promise.all([ Promise.all([this.startHeartbeat(), this.listenLaunchActor(), this.listenStopActor()]).catch((err: Error) => {
this.startHeartbeat(),
this.listenLaunchActor(),
this.listenStopActor()
]).catch((err: Error) => {
throw err; throw err;
}); });
} }
/**
* stopHost stops the heartbeat, stops all actors, drains the nats connections and disconnects from nats
*/
async stopHost() { async stopHost() {
// stop the heartbeat // stop the heartbeat
await this.stopHeartbeat(); await this.stopHeartbeat();
@ -226,22 +304,30 @@ export class Host {
} }
} }
/**
* startHost is the main function to start the js/browser host
*
* @param {string} name - the name of the host (defaults to 'default')
* @param {boolean} withRegistryTLS - whether or not remote registries use tls
* @param {Array<string>|ConnectionOptions} natsConnection - an array of nats websocket servers OR a full nats connection object
* @param {number} heartbeatInterval - used to determine the heartbeat to the lattice (defaults to 30000 or 30 seconds)
* @returns {Host}
*/
export async function startHost( export async function startHost(
name: string, name: string,
withRegistryTLS: boolean = true, withRegistryTLS: boolean = true,
natsConnection: Array<string> | ConnectionOptions, natsConnection: Array<string> | ConnectionOptions,
invocationCallbacks?: InvocationCallbacks,
heartbeatInterval?: number heartbeatInterval?: number
) { ) {
const wasmModule: any = await import('../wasmcloud-rs-js/pkg/'); const wasmModule: any = await import('../wasmcloud-rs-js/pkg/');
const wasm: any = await wasmModule.default; const wasm: any = await wasmModule.default;
const host: Host = new Host(name, const host: Host = new Host(
name,
withRegistryTLS, withRegistryTLS,
heartbeatInterval ? heartbeatInterval : HOST_HEARTBEAT_INTERVAL, heartbeatInterval ? heartbeatInterval : HOST_HEARTBEAT_INTERVAL,
natsConnection, natsConnection,
wasm, wasm
invocationCallbacks
); );
await host.startHost(); await host.startHost();
return host; return host;
} }

View File

@ -1 +1 @@
export { startHost } from './host'; export { startHost } from './host';

View File

@ -4,7 +4,7 @@ export type HeartbeatMessage = {
instances: number; instances: number;
}>; }>;
providers: []; providers: [];
} };
export type CreateLinkDefMessage = { export type CreateLinkDefMessage = {
actor_id: string; actor_id: string;
@ -12,7 +12,7 @@ export type CreateLinkDefMessage = {
link_name: string; link_name: string;
contract_id: string; contract_id: string;
values: any; values: any;
} };
export type ActorClaims = { export type ActorClaims = {
jti: string; jti: string;
@ -27,7 +27,7 @@ export type ActorClaims = {
ver: string; ver: string;
prov: boolean; prov: boolean;
}; };
} };
export type ActorClaimsMessage = { export type ActorClaimsMessage = {
call_alias: string; call_alias: string;
@ -38,33 +38,33 @@ export type ActorClaimsMessage = {
sub: string; sub: string;
tags: string; tags: string;
version: string; version: string;
} };
export type LaunchActorMessage = { export type LaunchActorMessage = {
actor_ref: string; actor_ref: string;
host_id: string; host_id: string;
} };
export type ActorStartedMessage = { export type ActorStartedMessage = {
api_version: number; api_version: number;
instance_id: string; instance_id: string;
public_key: string; public_key: string;
} };
export type ActorHealthCheckPassMessage = { export type ActorHealthCheckPassMessage = {
instance_id: string; instance_id: string;
public_key: string; public_key: string;
} };
export type StopActorMessage = { export type StopActorMessage = {
host_id: string; host_id: string;
actor_ref: string; actor_ref: string;
} };
export type ActorStoppedMessage = { export type ActorStoppedMessage = {
public_key: string; public_key: string;
instance_id: string; instance_id: string;
} };
export type InvocationMessage = { export type InvocationMessage = {
encoded_claims: string; encoded_claims: string;
@ -82,8 +82,13 @@ export type InvocationMessage = {
link_name: string; link_name: string;
contract_id: string; contract_id: string;
}; };
} };
export type InvocationCallbacks = { export type InvocationCallbacks = {
[key: string]: Function; [key: string]: Function;
} };
/* eslint-disable no-unused-vars */
export type HostCall = (binding: string, namespace: string, operation: string, payload: Uint8Array) => Uint8Array;
/* eslint-disable no-unused-vars */
export type Writer = (message: string) => void;

View File

@ -2,27 +2,56 @@ import { JSONCodec } from 'nats.ws';
const jc = JSONCodec(); const jc = JSONCodec();
/**
* uuidv4 returns a uuid string
*
* @returns {string}
*/
export function uuidv4(): string { export function uuidv4(): string {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); const r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16); return v.toString(16);
}); });
} }
/**
* parseJwt takes a jwt token and parses it into a json object
*
* @param token - the jwt token
* @returns {any} - the parsed jwt token with claims
*/
export function parseJwt(token: string) { export function parseJwt(token: string) {
var base64Url = token.split('.')[1]; var base64Url = token.split('.')[1];
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) { var jsonPayload = decodeURIComponent(
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2); atob(base64)
}).join('')); .split('')
.map(function (c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
})
.join('')
);
return JSON.parse(jsonPayload); return JSON.parse(jsonPayload);
} }
/**
* jsonEncode taks a json object and encodes it into uint8array for nats
*
* @param data - the data to encode
* @returns {Uint8Array}
*/
export function jsonEncode(data: any): Uint8Array { export function jsonEncode(data: any): Uint8Array {
return jc.encode(data); return jc.encode(data);
} }
/**
* jsonDecode decodes nats messages into json
*
* @param {Uint8Array} data - the nats encoded data
* @returns {any}
*/
export function jsonDecode(data: Uint8Array) { export function jsonDecode(data: Uint8Array) {
return jc.decode(data); return jc.decode(data);
} }

28
src/wasmbus.ts Normal file
View File

@ -0,0 +1,28 @@
import { WapcHost } from '@wapc/host';
import { HostCall, Writer } from './types';
export async function instantiate(source: Uint8Array, hostCall?: HostCall, writer?: Writer): Promise<Wasmbus> {
const host = new Wasmbus(hostCall, writer);
return host.instantiate(source);
}
export class Wasmbus extends WapcHost {
constructor(hostCall?: HostCall, writer?: Writer) {
super(hostCall, writer);
}
async instantiate(source: Uint8Array): Promise<Wasmbus> {
const imports = super.getImports();
const result = await WebAssembly.instantiate(source, {
wasmbus: imports.wapc,
wasi: imports.wasi,
wasi_unstable: imports.wasi_unstable
}).catch(e => {
throw new Error(`Invalid wasm binary: ${e.message}`);
});
super.initialize(result.instance);
return this;
}
}

View File

@ -11,9 +11,11 @@ const expect = chai.expect;
describe('wasmcloudjs', function () { describe('wasmcloudjs', function () {
it('should initialize a host with the name and key set', async () => { it('should initialize a host with the name and key set', async () => {
const host = await startHost('default', false, ["ws://localhost:4222"]); const host = await startHost('default', false, ['ws://localhost:4222']);
expect(host.name).to.equal('default'); expect(host.name).to.equal('default');
expect(host.key).to.be.a('string').and.satisfy((key: string) => key.startsWith('N')); expect(host.key)
.to.be.a('string')
.and.satisfy((key: string) => key.startsWith('N'));
expect(host.actors).to.equal({}); expect(host.actors).to.equal({});
}) });
}) });

View File

@ -0,0 +1,16 @@
version: "3"
services:
registry:
image: registry:2
ports:
- "5001:5001"
volumes:
- ./docker-registry.yml:/etc/docker/registry/config.yml
nats:
image: nats:latest
ports:
- "4222:4222"
- "6222:6222"
volumes:
- ./nats.conf:/etc/nats.conf
command: "-c=/etc/nats.conf -js"

View File

@ -0,0 +1,23 @@
version: 0.1
log:
fields:
service: registry
storage:
cache:
blobdescriptor: inmemory
filesystem:
rootdirectory: /var/lib/registry
http:
addr: :5000
headers:
X-Content-Type-Options: [nosniff]
Access-Control-Allow-Origin: ["*"]
health:
storagedriver:
enabled: true
interval: 10s
threshold: 3
cors:
origins: ["*"]
methods: ["GET", "PUT", "POST", "DELETE"]
headers: ["Access-Control-Allow-Origin", "Content-Type"]

6
test/infra/nats.conf Normal file
View File

@ -0,0 +1,6 @@
listen: localhost:4222
websocket {
# host: "hostname"
port: 6222
no_tls: true
}

View File

@ -1,7 +1,6 @@
{ {
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
"wasmcloud-rs-js/**/*.ts"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"
@ -25,6 +24,7 @@
"./wasmcloud-rs-js/pkg/" "./wasmcloud-rs-js/pkg/"
], ],
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true
} }
} }

View File

@ -14,7 +14,7 @@ crate-type = ["cdylib", "rlib"]
default = ["console_error_panic_hook"] default = ["console_error_panic_hook"]
[dependencies] [dependencies]
wasm-bindgen = "0.2.63" wasm-bindgen = "0.2.76"
wascap = "0.6.0" wascap = "0.6.0"
getrandom = { version = "0.2", features = ["js"] } getrandom = { version = "0.2", features = ["js"] }
rand = { version = "0.7.3", features = ["wasm-bindgen"] } rand = { version = "0.7.3", features = ["wasm-bindgen"] }

View File

@ -1,9 +1,25 @@
const path = require('path'); const path = require('path');
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin'); const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin');
const baseConfig = { const sharedConfig = {
stats: { assets: false, modules: false }, stats: { assets: false, modules: false, errors: true },
mode: 'production', mode: 'production',
resolve: {
extensions: ['.tsx', '.ts', '.js', '.wasm']
},
experiments: {
asyncWebAssembly: true
}
}
// this is specifically to use in a script tag
const browserConfig = {
output: {
webassemblyModuleFilename: 'wasmcloud.wasm',
filename: 'wasmcloud.js',
path: path.resolve(__dirname, 'dist'),
library: 'wasmcloudjs'
},
entry: './src/index.ts', entry: './src/index.ts',
module: { module: {
rules: [ rules: [
@ -14,44 +30,48 @@ const baseConfig = {
} }
] ]
}, },
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
plugins: [ plugins: [
new WasmPackPlugin({ new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, 'wasmcloud-rs-js'), crateDirectory: path.resolve(__dirname, 'wasmcloud-rs-js'),
extraArgs: '--target bundler', extraArgs: '--target bundler',
outDir: path.resolve(__dirname, 'wasmcloud-rs-js', 'pkg'), outDir: path.resolve(__dirname, 'wasmcloud-rs-js', 'pkg')
outName: 'wasmcloud_rs_js'
}) })
], ],
experiments: { ...sharedConfig
asyncWebAssembly: true
}
} }
const nodeConfig = { // this is used to bundle the rust wasm code in order to properly import into the compiled typescript code in the dist/src dir
target: 'node', // the tsc compiler handles the src code to cjs
const commonJSConfig = {
entry: './wasmcloud-rs-js/pkg/index.js',
output: { output: {
filename: 'index.node.js', webassemblyModuleFilename: 'wasmcloud.wasm',
path: path.resolve(__dirname, 'dist', 'src'), filename: 'index.js',
libraryTarget: 'umd', libraryTarget: 'commonjs2',
libraryExport: 'default', path: path.resolve(__dirname, 'dist', 'wasmcloud-rs-js', 'pkg')
library: 'wasmcloudjs' },
} module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
...sharedConfig
} }
const browserConfig = { module.exports = (env) => {
output: { switch (env.target) {
filename: 'index.bundle.js', case 'cjs':
path: path.resolve(__dirname, 'dist'), return commonJSConfig
library: 'wasmcloudjs' default:
return browserConfig
} }
}
module.exports = () => {
Object.assign(nodeConfig, baseConfig);
Object.assign(browserConfig, baseConfig);
return [browserConfig]
// return [browserConfig, nodeConfig];
}; };