Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

25 changed files with 215 additions and 2231 deletions

3
.gitignore vendored
View File

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

View File

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

113
README.md
View File

@ -1,6 +1,3 @@
> [!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.
@ -12,7 +9,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
* **seamlessly** bind the actor to the capability provider through Lattice
* **seemlessly** 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
@ -20,6 +17,7 @@ https://user-images.githubusercontent.com/1530656/130013412-b9a9daa6-fc71-424b-8
## Prerequisities
* NATS with WebSockets enabled
@ -47,107 +45,50 @@ $ 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="https://unpkg.com/@wasmcloud/wasmcloud-js@<VERSION>/dist/wasmcloud.js"></script>
<script src="dist/index.bundle.js"></script>
<script>
(async () => {
// 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
// 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
// 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, 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)
(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)
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();
// to start & stop the heartbeat events
await host.startHeartbeat(heartbeatInterval?);
await host.stopHeartbeat();
// 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
// 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
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* - NodeJS does not support WebSockets natively (required by nats.ws)
In progress--wasm-pack compile issues with nodeJS.
## Contributing

View File

@ -1,50 +0,0 @@
# 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

@ -1,18 +0,0 @@
// 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

@ -1,41 +0,0 @@
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

@ -1,22 +0,0 @@
<!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>

View File

@ -1,12 +0,0 @@
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,32 +1,31 @@
{
"name": "@wasmcloud/wasmcloud-js",
"version": "1.0.6",
"version": "1.0.0",
"description": "wasmcloud host in JavaScript/Browser",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"files": [
"dist",
"src",
"README.md",
"wasmcloud-rs-js"
"wasmcloud-rs-js",
"README.md"
],
"scripts": {
"build": "npm run clean && npm run build:browser && npm run build:cjs",
"build:browser": "webpack",
"build:cjs": "tsc --declaration && webpack --env target=cjs",
"build:browser": "webpack --mode=production",
"build:cjs": "tsc --declaration",
"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 --require ts-node/register",
"test": "mocha",
"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": {
@ -46,26 +45,24 @@
"extension": [
"ts"
],
"spec": "test/**/*.test.ts"
"spec": "test/**/*.test.ts",
"require": "ts-node/register"
},
"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",
@ -73,9 +70,9 @@
"webpack-cli": "^4.8.0"
},
"dependencies": {
"@msgpack/msgpack": "^2.7.1",
"@msgpack/msgpack": "^2.7.0",
"@wapc/host": "0.0.2",
"axios": "^0.24.0",
"axios": "^0.21.1",
"nats.ws": "^1.2.0"
}
}

View File

@ -1,42 +1,24 @@
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,
HostCall,
Writer
StopActorMessage
} from './types';
import { jsonEncode, parseJwt, uuidv4 } from './util';
/**
* Actor holds the actor wasm module
*/
export class Actor {
claims: ActorClaims;
key: string;
module!: Wasmbus;
module!: WapcHost;
hostKey: string;
hostName: string;
wasm: any;
invocationCallback?: Function;
hostCall?: HostCall;
writer?: Writer;
constructor(
hostName: string = 'default',
hostKey: string,
wasm: any,
invocationCallback?: Function,
hostCall?: HostCall,
writer?: Writer
) {
constructor(hostName: string = 'default', hostKey: string, wasm: any) {
this.key = '';
this.hostName = hostName;
this.hostKey = hostKey;
@ -55,16 +37,8 @@ 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);
@ -73,27 +47,17 @@ export class Actor {
}
this.claims = parseJwt(token);
this.key = this.claims.sub;
this.module = await instantiate(actorBuffer, this.hostCall, this.writer);
this.module = await instantiate(actorBuffer);
}
/**
* 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 = {
@ -105,85 +69,59 @@ 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)));
}
/**
* subscribeInvocations does a subscribe on nats for invocations
*
* @param {NatsConnection} natsConn the nats connection object
*/
async subscribeInvocations(natsConn: NatsConnection) {
async subscribeInvocations(natsConn: NatsConnection, invocationCallback?: Function) {
// 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 (this.invocationCallback) {
this.invocationCallback(invocationResult);
invocationMessage.respond(encode({
invocation_id: (invocationData as any).id,
instance_id: uuidv4(),
msg: invocationResult
}));
if (invocationCallback) {
invocationCallback(invocationResult);
}
}
throw new Error('actor.inovcation subscription closed');
}
}
/**
* 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,
export async function newActor(hostName: string, hostKey: string,
actorModule: Uint8Array,
natsConn: NatsConnection,
wasm: any,
invocationCallback?: Function,
hostCall?: HostCall,
writer?: Writer
invocationCallback?: Function
): Promise<Actor> {
const actor: Actor = new Actor(hostName, hostKey, wasm, invocationCallback, hostCall, writer);
const actor: Actor = new Actor(hostName, hostKey, wasm);
await actor.startActor(actorModule);
await actor.publishActorStarted(natsConn);
Promise.all([actor.subscribeInvocations(natsConn)]).catch(err => {
Promise.all([
actor.subscribeInvocations(natsConn, invocationCallback)
]).catch((err) => {
throw err;
});
return actor;
}
}

View File

@ -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,16 +15,8 @@ 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,
@ -34,5 +26,5 @@ export function createEventMessage(hostKey: string, eventType: EventType, data:
specversion: '1.0',
time: new Date().toISOString(),
type: eventType
};
}
}
}

View File

@ -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,27 +28,18 @@ 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) {
@ -59,23 +50,17 @@ 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);
}

View File

@ -1,7 +1,11 @@
import { encode } from '@msgpack/msgpack';
import { connect, ConnectionOptions, NatsConnection, Subscription } from 'nats.ws';
import {
connect, ConnectionOptions,
NatsConnection,
Subscription
} from 'nats.ws';
import { Actor, startActor } from './actor';
import { Actor, newActor } from './actor';
import { createEventMessage, EventType } from './events';
import { fetchActor, fetchActorDigest, ImageDigest } from './fetch';
import {
@ -10,23 +14,19 @@ import {
HeartbeatMessage,
InvocationCallbacks,
LaunchActorMessage,
StopActorMessage,
HostCall,
Writer
StopActorMessage
} 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,20 +38,14 @@ 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
wasm: any,
invocationCallbacks?: InvocationCallbacks
) {
const hostKey = new wasm.HostKey();
this.name = name;
@ -62,68 +56,46 @@ export class Host {
this.wasm = wasm;
this.heartbeatInterval = heartbeatInterval;
this.natsConnOpts = natsConnOpts;
this.invocationCallbacks = {};
this.hostCalls = {};
this.writers = {};
this.invocationCallbacks = invocationCallbacks;
}
/**
* 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) {
@ -133,55 +105,28 @@ 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;
}
/**
* 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) {
async launchActor(actorRef: string) {
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
@ -197,22 +142,16 @@ 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 startActor(
this.name,
this.key,
const actor: Actor = await newActor(this.name, this.key,
actorModule,
this.natsConn,
this.wasm,
this.invocationCallbacks?.[actorRef],
this.hostCalls?.[actorRef],
this.writers?.[actorRef]
this.invocationCallbacks?.[actorRef]
);
if (this.actors[actorRef]) {
@ -221,49 +160,34 @@ 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 and remove the invocation callback
// delete the actor from the host
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,
@ -271,23 +195,21 @@ 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();
@ -304,30 +226,22 @@ 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
wasm,
invocationCallbacks
);
await host.startHost();
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;
}>;
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,13 +82,8 @@ 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;
}

View File

@ -2,56 +2,27 @@ 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);
}
}

View File

@ -1,28 +0,0 @@
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,11 +11,9 @@ 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({});
});
});
})
})

View File

@ -1,16 +0,0 @@
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

@ -1,23 +0,0 @@
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"]

View File

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

View File

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

View File

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

View File

@ -1,25 +1,9 @@
const path = require('path');
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin');
const sharedConfig = {
stats: { assets: false, modules: false, errors: true },
const baseConfig = {
stats: { assets: false, modules: false },
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: [
@ -30,48 +14,44 @@ const browserConfig = {
}
]
},
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')
outDir: path.resolve(__dirname, 'wasmcloud-rs-js', 'pkg'),
outName: 'wasmcloud_rs_js'
})
],
...sharedConfig
}
// 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: {
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
}
module.exports = (env) => {
switch (env.target) {
case 'cjs':
return commonJSConfig
default:
return browserConfig
experiments: {
asyncWebAssembly: true
}
}
const nodeConfig = {
target: 'node',
output: {
filename: 'index.node.js',
path: path.resolve(__dirname, 'dist', 'src'),
libraryTarget: 'umd',
libraryExport: 'default',
library: 'wasmcloudjs'
}
}
const browserConfig = {
output: {
filename: 'index.bundle.js',
path: path.resolve(__dirname, 'dist'),
library: 'wasmcloudjs'
}
}
module.exports = () => {
Object.assign(nodeConfig, baseConfig);
Object.assign(browserConfig, baseConfig);
return [browserConfig]
// return [browserConfig, nodeConfig];
};