Compare commits
24 Commits
Author | SHA1 | Date |
---|---|---|
|
e9ec9adf81 | |
|
b241df0590 | |
|
de97386d27 | |
|
44c14b73cb | |
|
daa3a7e099 | |
|
1d33dd97f7 | |
|
8d870d1546 | |
|
197e0ce212 | |
|
2457fdf000 | |
|
474559a0a6 | |
|
52fb0475ec | |
|
6d807b1892 | |
|
2f5719cca1 | |
|
e9f6d37854 | |
|
28f1232235 | |
|
8ac0419b59 | |
|
51a68841a7 | |
|
0810351fda | |
|
43979140b4 | |
|
fdbc218b04 | |
|
c55c160a83 | |
|
aaaf614c0f | |
|
e9f626a6f2 | |
|
7fabaadf28 |
|
@ -1,5 +1,6 @@
|
|||
node_modules/*
|
||||
dist/*
|
||||
index.html
|
||||
!examples/**/*index.html*
|
||||
*.sh*
|
||||
wasmcloud-rs-js/pkg/
|
||||
wasmcloud-rs-js/pkg/
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"trailingComma": "all",
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
|
|
113
README.md
113
README.md
|
@ -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
|
||||
|
||||
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 the wasmcloud-js host into a 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
|
||||
* Unload the actor
|
||||
|
||||
|
@ -17,7 +20,6 @@ https://user-images.githubusercontent.com/1530656/130013412-b9a9daa6-fc71-424b-8
|
|||
|
||||
|
||||
|
||||
|
||||
## Prerequisities
|
||||
|
||||
* NATS with WebSockets enabled
|
||||
|
@ -45,50 +47,107 @@ $ npm install @wasmcloud/wasmcloud-js
|
|||
|
||||
## Usage
|
||||
|
||||
More examples can be found in the [examples](examples/) directory, including sample `webpack` and `esbuild` configurations
|
||||
|
||||
**Browser**
|
||||
|
||||
```html
|
||||
<script src="dist/index.bundle.js"></script>
|
||||
<script src="https://unpkg.com/@wasmcloud/wasmcloud-js@<VERSION>/dist/wasmcloud.js"></script>
|
||||
<script>
|
||||
(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)
|
||||
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
|
||||
// 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);
|
||||
// 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
|
||||
(async() => {
|
||||
await host.listenLaunchActor(
|
||||
{
|
||||
"localhost:5000/echo:0.2.2": (data, result) => console.log(data.operation, result);
|
||||
}
|
||||
);
|
||||
await host.listenStopActor();
|
||||
})();
|
||||
// to launch an actor manually from the library from a registry
|
||||
await host.launchActor("registry.com/actor:0.1.1")
|
||||
// to launch an actrom manually from local disk (note the .wasm is required)
|
||||
// (async() => {
|
||||
// await host.listenLaunchActor(
|
||||
// {
|
||||
// "localhost:5000/echo:0.2.2": (data, result) => console.log(data.operation, result);
|
||||
// }
|
||||
// );
|
||||
// await host.listenStopActor();
|
||||
// })();
|
||||
// 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.
|
||||
// The hostCallback format is as follows:
|
||||
// ```
|
||||
// (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");
|
||||
// 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));
|
||||
// to unsubscribe, call the unsubscribeEvents
|
||||
// To unsubscribe, call the unsubscribeEvents
|
||||
await host.unsubscribeEvents();
|
||||
// to start & stop the heartbeat events
|
||||
await host.startHeartbeat(heartbeatInterval?);
|
||||
// To start & stop the heartbeat events
|
||||
await host.startHeartbeat();
|
||||
await host.stopHeartbeat();
|
||||
// the host will automatically connect to nats on start. to connect/reconnect to nats
|
||||
await host.connectNATS(["ws://locahost:4222/"], {});
|
||||
// to close/drain all connections from nats, call the disconnectNATS() method
|
||||
// The host will automatically connect to nats on start. to connect/reconnect to nats
|
||||
await host.connectNATS();
|
||||
// To close/drain all connections from nats, call the disconnectNATS() method
|
||||
await host.disconnectNATS();
|
||||
// stop the host
|
||||
// Stop the host
|
||||
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();
|
||||
})();
|
||||
</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**
|
||||
|
||||
In progress--wasm-pack compile issues with nodeJS.
|
||||
*IN PROGRESS* - NodeJS does not support WebSockets natively (required by nats.ws)
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
@ -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).
|
|
@ -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))
|
|
@ -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, '')
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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);
|
||||
})()
|
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
|
@ -1,31 +1,32 @@
|
|||
{
|
||||
"name": "@wasmcloud/wasmcloud-js",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.6",
|
||||
"description": "wasmcloud host in JavaScript/Browser",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
"files": [
|
||||
"dist",
|
||||
"src",
|
||||
"wasmcloud-rs-js",
|
||||
"README.md"
|
||||
"README.md",
|
||||
"wasmcloud-rs-js"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run clean && npm run build:browser && npm run build:cjs",
|
||||
"build:browser": "webpack --mode=production",
|
||||
"build:cjs": "tsc --declaration",
|
||||
"build:browser": "webpack",
|
||||
"build:cjs": "tsc --declaration && webpack --env target=cjs",
|
||||
"build:wasm": "cd wasmcloud-rs-js && wasm-pack build",
|
||||
"clean": "rm -rf ./dist/ && rm -rf ./wasmcloud-rs-js/pkg/",
|
||||
"lint": "eslint --ext .ts src test",
|
||||
"format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
|
||||
"test": "mocha",
|
||||
"test": "mocha --require ts-node/register",
|
||||
"watch": "npm run clean && tsc -w --declrataion",
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"prettier": ".prettierrc.json",
|
||||
"prettier": "./.prettierrc.json",
|
||||
"keywords": [
|
||||
"wasmcloud",
|
||||
"wasmcloud-host",
|
||||
"wasmcloud-js",
|
||||
"wasm"
|
||||
],
|
||||
"eslintConfig": {
|
||||
|
@ -45,24 +46,26 @@
|
|||
"extension": [
|
||||
"ts"
|
||||
],
|
||||
"spec": "test/**/*.test.ts",
|
||||
"require": "ts-node/register"
|
||||
"spec": "test/**/*.test.ts"
|
||||
},
|
||||
"author": "ks2211 <kaushik@cosmonic.com>",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.15.0",
|
||||
"@babel/preset-env": "^7.15.0",
|
||||
"@types/chai": "^4.2.21",
|
||||
"@types/chai-as-promised": "^7.1.4",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.22.0",
|
||||
"@typescript-eslint/parser": "^4.29.2",
|
||||
"@wasm-tool/wasm-pack-plugin": "^1.5.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
"chai": "^4.3.4",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"esbuild": "^0.12.20",
|
||||
"eslint": "^7.32.0",
|
||||
"mocha": "^9.0.3",
|
||||
"path": "^0.12.7",
|
||||
"prettier": "^2.3.2",
|
||||
"ts-loader": "^9.2.5",
|
||||
"ts-node": "^10.2.1",
|
||||
"typescript": "^4.3.5",
|
||||
|
@ -70,9 +73,9 @@
|
|||
"webpack-cli": "^4.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@msgpack/msgpack": "^2.7.0",
|
||||
"@msgpack/msgpack": "^2.7.1",
|
||||
"@wapc/host": "0.0.2",
|
||||
"axios": "^0.21.1",
|
||||
"axios": "^0.24.0",
|
||||
"nats.ws": "^1.2.0"
|
||||
}
|
||||
}
|
126
src/actor.ts
126
src/actor.ts
|
@ -1,24 +1,42 @@
|
|||
import { encode, decode } from '@msgpack/msgpack';
|
||||
import { instantiate, WapcHost } from '@wapc/host';
|
||||
import { NatsConnection, Subscription } from 'nats.ws';
|
||||
|
||||
import { instantiate, Wasmbus } from './wasmbus';
|
||||
import { createEventMessage, EventType } from './events';
|
||||
import {
|
||||
ActorClaims, ActorClaimsMessage, ActorStartedMessage, ActorHealthCheckPassMessage,
|
||||
ActorClaims,
|
||||
ActorClaimsMessage,
|
||||
ActorStartedMessage,
|
||||
ActorHealthCheckPassMessage,
|
||||
InvocationMessage,
|
||||
StopActorMessage
|
||||
StopActorMessage,
|
||||
HostCall,
|
||||
Writer
|
||||
} from './types';
|
||||
import { jsonEncode, parseJwt, uuidv4 } from './util';
|
||||
|
||||
/**
|
||||
* Actor holds the actor wasm module
|
||||
*/
|
||||
export class Actor {
|
||||
claims: ActorClaims;
|
||||
key: string;
|
||||
module!: WapcHost;
|
||||
module!: Wasmbus;
|
||||
hostKey: string;
|
||||
hostName: string;
|
||||
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.hostName = hostName;
|
||||
this.hostKey = hostKey;
|
||||
|
@ -37,8 +55,16 @@ export class Actor {
|
|||
}
|
||||
};
|
||||
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) {
|
||||
const token: string = await this.wasm.extract_jwt(actorBuffer);
|
||||
const valid: boolean = await this.wasm.validate_jwt(token);
|
||||
|
@ -47,17 +73,27 @@ export class Actor {
|
|||
}
|
||||
this.claims = parseJwt(token);
|
||||
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) {
|
||||
const actorToStop: StopActorMessage = {
|
||||
host_id: this.hostKey,
|
||||
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) {
|
||||
// publish claims
|
||||
const claims: ActorClaimsMessage = {
|
||||
|
@ -69,59 +105,85 @@ export class Actor {
|
|||
sub: this.claims.sub,
|
||||
tags: '',
|
||||
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
|
||||
const actorStarted: ActorStartedMessage = {
|
||||
api_version: 0,
|
||||
instance_id: uuidv4(),
|
||||
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
|
||||
const actorHealthCheck: ActorHealthCheckPassMessage = {
|
||||
instance_id: uuidv4(),
|
||||
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
|
||||
const invocationsTopic: Subscription = natsConn.subscribe(`wasmbus.rpc.${this.hostName}.${this.key}`);
|
||||
for await (const invocationMessage of invocationsTopic) {
|
||||
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);
|
||||
invocationMessage.respond(encode({
|
||||
invocation_id: (invocationData as any).id,
|
||||
instance_id: uuidv4(),
|
||||
msg: invocationResult
|
||||
}));
|
||||
if (invocationCallback) {
|
||||
invocationCallback(invocationResult);
|
||||
invocationMessage.respond(
|
||||
encode({
|
||||
invocation_id: (invocationData as any).id,
|
||||
instance_id: uuidv4(),
|
||||
msg: invocationResult
|
||||
})
|
||||
);
|
||||
if (this.invocationCallback) {
|
||||
this.invocationCallback(invocationResult);
|
||||
}
|
||||
}
|
||||
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,
|
||||
natsConn: NatsConnection,
|
||||
wasm: any,
|
||||
invocationCallback?: Function
|
||||
invocationCallback?: Function,
|
||||
hostCall?: HostCall,
|
||||
writer?: Writer
|
||||
): 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.publishActorStarted(natsConn);
|
||||
Promise.all([
|
||||
actor.subscribeInvocations(natsConn, invocationCallback)
|
||||
]).catch((err) => {
|
||||
Promise.all([actor.subscribeInvocations(natsConn)]).catch(err => {
|
||||
throw err;
|
||||
});
|
||||
return actor;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { uuidv4 } from './util'
|
||||
import { uuidv4 } from './util';
|
||||
|
||||
export enum EventType {
|
||||
HeartBeat = 'com.wasmcloud.lattice.host_heartbeat',
|
||||
ActorStarted = 'com.wasmcloud.lattice.actor_started',
|
||||
ActorStopped = 'com.wasmcloud.lattice.actor_stopped',
|
||||
HealthCheckPass = 'com.wasmcloud.lattice.health_check_passed',
|
||||
HealthCheckPass = 'com.wasmcloud.lattice.health_check_passed'
|
||||
}
|
||||
|
||||
export type EventData = {
|
||||
|
@ -15,8 +15,16 @@ export type EventData = {
|
|||
time: string;
|
||||
type: EventType;
|
||||
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 {
|
||||
return {
|
||||
data: data,
|
||||
|
@ -26,5 +34,5 @@ export function createEventMessage(hostKey: string, eventType: EventType, data:
|
|||
specversion: '1.0',
|
||||
time: new Date().toISOString(),
|
||||
type: eventType
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
43
src/fetch.ts
43
src/fetch.ts
|
@ -4,7 +4,7 @@ export type ImageDigest = {
|
|||
name: string;
|
||||
digest: string;
|
||||
registry: string;
|
||||
}
|
||||
};
|
||||
|
||||
type FetchActorDigestResponse = {
|
||||
schemaVersion: number;
|
||||
|
@ -19,7 +19,7 @@ type FetchActorDigestResponse = {
|
|||
layers: Array<{
|
||||
annotations: {
|
||||
['org.opencontainers.image.title']: string;
|
||||
}
|
||||
};
|
||||
digest: string;
|
||||
mediaType: string;
|
||||
size: number;
|
||||
|
@ -28,18 +28,27 @@ type FetchActorDigestResponse = {
|
|||
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> {
|
||||
const image: Array<string> = actorRef.split('/');
|
||||
const registry: string = image[0];
|
||||
const [name, version] = image[1].split(':');
|
||||
|
||||
const response: AxiosResponse = await axios.get(
|
||||
`${withTLS ? 'https://' : 'http://'}${registry}/v2/${name}/manifests/${version}`,
|
||||
{
|
||||
const response: AxiosResponse = await axios
|
||||
.get(`${withTLS ? 'https://' : 'http://'}${registry}/v2/${name}/manifests/${version}`, {
|
||||
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;
|
||||
|
||||
if (layers.layers.length === 0) {
|
||||
|
@ -50,17 +59,23 @@ export async function fetchActorDigest(actorRef: string, withTLS?: boolean): Pro
|
|||
name,
|
||||
digest: layers.layers[0].digest,
|
||||
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> {
|
||||
const response: AxiosResponse = await axios.get(
|
||||
url,
|
||||
{
|
||||
const response: AxiosResponse = await axios
|
||||
.get(url, {
|
||||
responseType: 'arraybuffer'
|
||||
}
|
||||
).catch((err) => { throw err });
|
||||
})
|
||||
.catch(err => {
|
||||
throw err;
|
||||
});
|
||||
|
||||
return new Uint8Array(response.data);
|
||||
}
|
||||
|
||||
|
|
182
src/host.ts
182
src/host.ts
|
@ -1,11 +1,7 @@
|
|||
import { encode } from '@msgpack/msgpack';
|
||||
import {
|
||||
connect, ConnectionOptions,
|
||||
NatsConnection,
|
||||
Subscription
|
||||
} from 'nats.ws';
|
||||
import { connect, ConnectionOptions, NatsConnection, Subscription } from 'nats.ws';
|
||||
|
||||
import { Actor, newActor } from './actor';
|
||||
import { Actor, startActor } from './actor';
|
||||
import { createEventMessage, EventType } from './events';
|
||||
import { fetchActor, fetchActorDigest, ImageDigest } from './fetch';
|
||||
import {
|
||||
|
@ -14,19 +10,23 @@ import {
|
|||
HeartbeatMessage,
|
||||
InvocationCallbacks,
|
||||
LaunchActorMessage,
|
||||
StopActorMessage
|
||||
StopActorMessage,
|
||||
HostCall,
|
||||
Writer
|
||||
} from './types';
|
||||
import { jsonDecode, jsonEncode, uuidv4 } from './util';
|
||||
|
||||
const HOST_HEARTBEAT_INTERVAL: number = 30000;
|
||||
|
||||
/**
|
||||
* Host holds the js/browser host
|
||||
*/
|
||||
export class Host {
|
||||
name: string;
|
||||
key: string;
|
||||
seed: string;
|
||||
heartbeatInterval: number;
|
||||
heartbeatIntervalId: any;
|
||||
invocationCallbacks?: InvocationCallbacks;
|
||||
withRegistryTLS: boolean;
|
||||
actors: {
|
||||
[key: string]: {
|
||||
|
@ -38,14 +38,20 @@ export class Host {
|
|||
natsConn!: NatsConnection;
|
||||
eventsSubscription!: Subscription | null;
|
||||
wasm: any;
|
||||
invocationCallbacks?: InvocationCallbacks;
|
||||
hostCalls?: {
|
||||
[key: string]: HostCall;
|
||||
};
|
||||
writers?: {
|
||||
[key: string]: Writer;
|
||||
};
|
||||
|
||||
|
||||
constructor(name: string = 'default',
|
||||
constructor(
|
||||
name: string = 'default',
|
||||
withRegistryTLS: boolean,
|
||||
heartbeatInterval: number,
|
||||
natsConnOpts: Array<string> | ConnectionOptions,
|
||||
wasm: any,
|
||||
invocationCallbacks?: InvocationCallbacks
|
||||
wasm: any
|
||||
) {
|
||||
const hostKey = new wasm.HostKey();
|
||||
this.name = name;
|
||||
|
@ -56,46 +62,68 @@ export class Host {
|
|||
this.wasm = wasm;
|
||||
this.heartbeatInterval = heartbeatInterval;
|
||||
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() {
|
||||
const opts: ConnectionOptions = (Array.isArray(this.natsConnOpts)) ? {
|
||||
servers: this.natsConnOpts
|
||||
} : this.natsConnOpts;
|
||||
const opts: ConnectionOptions = Array.isArray(this.natsConnOpts)
|
||||
? {
|
||||
servers: this.natsConnOpts
|
||||
}
|
||||
: this.natsConnOpts;
|
||||
this.natsConn = await connect(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* disconnectNATS disconnects from nats
|
||||
*/
|
||||
async disconnectNATS() {
|
||||
this.natsConn.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* startHeartbeat starts a heartbeat publish message every X seconds based on the interval
|
||||
*/
|
||||
async startHeartbeat() {
|
||||
this.heartbeatIntervalId;
|
||||
const heartbeat: HeartbeatMessage = {
|
||||
actors: [],
|
||||
providers: []
|
||||
}
|
||||
};
|
||||
for (const actor in this.actors) {
|
||||
heartbeat.actors.push({
|
||||
actor: actor,
|
||||
instances: 1
|
||||
})
|
||||
});
|
||||
}
|
||||
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))
|
||||
);
|
||||
}, this.heartbeatInterval)
|
||||
}, this.heartbeatInterval);
|
||||
}
|
||||
|
||||
/**
|
||||
* stopHeartbeat clears the heartbeat interval
|
||||
*/
|
||||
async stopHeartbeat() {
|
||||
clearInterval(this.heartbeatIntervalId);
|
||||
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) {
|
||||
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) {
|
||||
const eventData = jsonDecode(event.data);
|
||||
if (eventCallback) {
|
||||
|
@ -105,28 +133,55 @@ export class Host {
|
|||
throw new Error('evt subscription was closed');
|
||||
}
|
||||
|
||||
/**
|
||||
* unsubscribeEvents unsubscribes and removes the events subscription
|
||||
*/
|
||||
async unsubscribeEvents() {
|
||||
this.eventsSubscription?.unsubscribe();
|
||||
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 = {
|
||||
actor_ref: actorRef,
|
||||
host_id: this.key
|
||||
}
|
||||
};
|
||||
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) {
|
||||
const actorToStop: StopActorMessage = {
|
||||
host_id: this.key,
|
||||
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() {
|
||||
// subscribe to the .la topic `wasmbus.ctl.${this.name}.cmd.${this.key}.la`
|
||||
// decode the data
|
||||
|
@ -142,16 +197,22 @@ export class Host {
|
|||
let url: string;
|
||||
if (usingRegistry) {
|
||||
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 {
|
||||
url = actorRef;
|
||||
}
|
||||
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,
|
||||
this.natsConn,
|
||||
this.wasm,
|
||||
this.invocationCallbacks?.[actorRef]
|
||||
this.invocationCallbacks?.[actorRef],
|
||||
this.hostCalls?.[actorRef],
|
||||
this.writers?.[actorRef]
|
||||
);
|
||||
|
||||
if (this.actors[actorRef]) {
|
||||
|
@ -160,34 +221,49 @@ export class Host {
|
|||
this.actors[actorRef] = {
|
||||
count: 1,
|
||||
actor: actor
|
||||
}
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
// TODO: error handling
|
||||
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() {
|
||||
// listen for stop actor message, decode the data
|
||||
// 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`);
|
||||
for await (const actorMessage of actorsTopic) {
|
||||
const actorData = jsonDecode(actorMessage.data);
|
||||
const actorStop: ActorStoppedMessage = {
|
||||
instance_id: uuidv4(),
|
||||
public_key: this.actors[(actorData as StopActorMessage).actor_ref].actor.key
|
||||
}
|
||||
this.natsConn.publish(`wasmbus.evt.${this.name}`,
|
||||
jsonEncode(createEventMessage(this.key, EventType.ActorStopped, actorStop)))
|
||||
};
|
||||
this.natsConn.publish(
|
||||
`wasmbus.evt.${this.name}`,
|
||||
jsonEncode(createEventMessage(this.key, EventType.ActorStopped, actorStop))
|
||||
);
|
||||
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) {
|
||||
const linkDefinition: CreateLinkDefMessage = {
|
||||
actor_id: actorKey,
|
||||
|
@ -195,21 +271,23 @@ export class Host {
|
|||
link_name: linkName,
|
||||
contract_id: contractId,
|
||||
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() {
|
||||
await this.connectNATS();
|
||||
Promise.all([
|
||||
this.startHeartbeat(),
|
||||
this.listenLaunchActor(),
|
||||
this.listenStopActor()
|
||||
]).catch((err: Error) => {
|
||||
Promise.all([this.startHeartbeat(), this.listenLaunchActor(), this.listenStopActor()]).catch((err: Error) => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* stopHost stops the heartbeat, stops all actors, drains the nats connections and disconnects from nats
|
||||
*/
|
||||
async stopHost() {
|
||||
// stop the heartbeat
|
||||
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(
|
||||
name: string,
|
||||
withRegistryTLS: boolean = true,
|
||||
natsConnection: Array<string> | ConnectionOptions,
|
||||
invocationCallbacks?: InvocationCallbacks,
|
||||
heartbeatInterval?: number
|
||||
) {
|
||||
const wasmModule: any = await import('../wasmcloud-rs-js/pkg/');
|
||||
const wasm: any = await wasmModule.default;
|
||||
const host: Host = new Host(name,
|
||||
const host: Host = new Host(
|
||||
name,
|
||||
withRegistryTLS,
|
||||
heartbeatInterval ? heartbeatInterval : HOST_HEARTBEAT_INTERVAL,
|
||||
natsConnection,
|
||||
wasm,
|
||||
invocationCallbacks
|
||||
wasm
|
||||
);
|
||||
await host.startHost();
|
||||
return host;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
export { startHost } from './host';
|
||||
export { startHost } from './host';
|
||||
|
|
27
src/types.ts
27
src/types.ts
|
@ -4,7 +4,7 @@ export type HeartbeatMessage = {
|
|||
instances: number;
|
||||
}>;
|
||||
providers: [];
|
||||
}
|
||||
};
|
||||
|
||||
export type CreateLinkDefMessage = {
|
||||
actor_id: string;
|
||||
|
@ -12,7 +12,7 @@ export type CreateLinkDefMessage = {
|
|||
link_name: string;
|
||||
contract_id: string;
|
||||
values: any;
|
||||
}
|
||||
};
|
||||
|
||||
export type ActorClaims = {
|
||||
jti: string;
|
||||
|
@ -27,7 +27,7 @@ export type ActorClaims = {
|
|||
ver: string;
|
||||
prov: boolean;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export type ActorClaimsMessage = {
|
||||
call_alias: string;
|
||||
|
@ -38,33 +38,33 @@ export type ActorClaimsMessage = {
|
|||
sub: string;
|
||||
tags: string;
|
||||
version: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type LaunchActorMessage = {
|
||||
actor_ref: string;
|
||||
host_id: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type ActorStartedMessage = {
|
||||
api_version: number;
|
||||
instance_id: string;
|
||||
public_key: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type ActorHealthCheckPassMessage = {
|
||||
instance_id: string;
|
||||
public_key: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type StopActorMessage = {
|
||||
host_id: string;
|
||||
actor_ref: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type ActorStoppedMessage = {
|
||||
public_key: string;
|
||||
instance_id: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type InvocationMessage = {
|
||||
encoded_claims: string;
|
||||
|
@ -82,8 +82,13 @@ export type InvocationMessage = {
|
|||
link_name: string;
|
||||
contract_id: string;
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export type InvocationCallbacks = {
|
||||
[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;
|
||||
|
|
39
src/util.ts
39
src/util.ts
|
@ -2,27 +2,56 @@ import { JSONCodec } from 'nats.ws';
|
|||
|
||||
const jc = JSONCodec();
|
||||
|
||||
/**
|
||||
* uuidv4 returns a uuid string
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
export function uuidv4(): string {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
var base64Url = token.split('.')[1];
|
||||
var base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
var jsonPayload = decodeURIComponent(atob(base64).split('').map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
var jsonPayload = decodeURIComponent(
|
||||
atob(base64)
|
||||
.split('')
|
||||
.map(function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
})
|
||||
.join('')
|
||||
);
|
||||
|
||||
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 {
|
||||
return jc.encode(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* jsonDecode decodes nats messages into json
|
||||
*
|
||||
* @param {Uint8Array} data - the nats encoded data
|
||||
* @returns {any}
|
||||
*/
|
||||
export function jsonDecode(data: Uint8Array) {
|
||||
return jc.decode(data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -11,9 +11,11 @@ const expect = chai.expect;
|
|||
|
||||
describe('wasmcloudjs', function () {
|
||||
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.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({});
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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"
|
|
@ -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"]
|
|
@ -0,0 +1,6 @@
|
|||
listen: localhost:4222
|
||||
websocket {
|
||||
# host: "hostname"
|
||||
port: 6222
|
||||
no_tls: true
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"wasmcloud-rs-js/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
|
@ -25,6 +24,7 @@
|
|||
"./wasmcloud-rs-js/pkg/"
|
||||
],
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
}
|
||||
}
|
|
@ -14,7 +14,7 @@ crate-type = ["cdylib", "rlib"]
|
|||
default = ["console_error_panic_hook"]
|
||||
|
||||
[dependencies]
|
||||
wasm-bindgen = "0.2.63"
|
||||
wasm-bindgen = "0.2.76"
|
||||
wascap = "0.6.0"
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
rand = { version = "0.7.3", features = ["wasm-bindgen"] }
|
||||
|
|
|
@ -1,9 +1,25 @@
|
|||
const path = require('path');
|
||||
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin');
|
||||
|
||||
const baseConfig = {
|
||||
stats: { assets: false, modules: false },
|
||||
const sharedConfig = {
|
||||
stats: { assets: false, modules: false, errors: true },
|
||||
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',
|
||||
module: {
|
||||
rules: [
|
||||
|
@ -14,44 +30,48 @@ const baseConfig = {
|
|||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.tsx', '.ts', '.js']
|
||||
},
|
||||
plugins: [
|
||||
new WasmPackPlugin({
|
||||
crateDirectory: path.resolve(__dirname, 'wasmcloud-rs-js'),
|
||||
extraArgs: '--target bundler',
|
||||
outDir: path.resolve(__dirname, 'wasmcloud-rs-js', 'pkg'),
|
||||
outName: 'wasmcloud_rs_js'
|
||||
outDir: path.resolve(__dirname, 'wasmcloud-rs-js', 'pkg')
|
||||
})
|
||||
],
|
||||
experiments: {
|
||||
asyncWebAssembly: true
|
||||
}
|
||||
...sharedConfig
|
||||
}
|
||||
|
||||
const nodeConfig = {
|
||||
target: 'node',
|
||||
// this is used to bundle the rust wasm code in order to properly import into the compiled typescript code in the dist/src dir
|
||||
// the tsc compiler handles the src code to cjs
|
||||
const commonJSConfig = {
|
||||
entry: './wasmcloud-rs-js/pkg/index.js',
|
||||
output: {
|
||||
filename: 'index.node.js',
|
||||
path: path.resolve(__dirname, 'dist', 'src'),
|
||||
libraryTarget: 'umd',
|
||||
libraryExport: 'default',
|
||||
library: 'wasmcloudjs'
|
||||
}
|
||||
webassemblyModuleFilename: 'wasmcloud.wasm',
|
||||
filename: 'index.js',
|
||||
libraryTarget: 'commonjs2',
|
||||
path: path.resolve(__dirname, 'dist', 'wasmcloud-rs-js', 'pkg')
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /(node_modules)/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: ['@babel/preset-env']
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
...sharedConfig
|
||||
}
|
||||
|
||||
const browserConfig = {
|
||||
output: {
|
||||
filename: 'index.bundle.js',
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
library: 'wasmcloudjs'
|
||||
module.exports = (env) => {
|
||||
switch (env.target) {
|
||||
case 'cjs':
|
||||
return commonJSConfig
|
||||
default:
|
||||
return browserConfig
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = () => {
|
||||
Object.assign(nodeConfig, baseConfig);
|
||||
Object.assign(browserConfig, baseConfig);
|
||||
return [browserConfig]
|
||||
// return [browserConfig, nodeConfig];
|
||||
};
|
Loading…
Reference in New Issue