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

1
.gitignore vendored
View File

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

View File

@ -1,6 +1,6 @@
{ {
"$schema": "https://json.schemastore.org/prettierrc", "$schema": "https://json.schemastore.org/prettierrc",
"trailingComma": "none", "trailingComma": "all",
"tabWidth": 2, "tabWidth": 2,
"semi": true, "semi": true,
"singleQuote": 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 # wasmCloud Host in JavaScript/Browser
This is the JavaScript implementation of the wasmCloud host for the browser (NodeJS support in progress). The library runs a host inside a web browser/application that connects to a remote lattice via NATS and can run wasmCloud actors in the browser. The host will automatically listen for actor start/stop from NATS and will initialize the actor using `wapcJS`. Any invocations will be handled by the browser actor and returned to the requesting host via NATS. Users can pass callbacks to handle invocation and event data. This is the JavaScript implementation of the wasmCloud host for the browser (NodeJS support in progress). The library runs a host inside a web browser/application that connects to a remote lattice via NATS and can run wasmCloud actors in the browser. The host will automatically listen for actor start/stop from NATS and will initialize the actor using `wapcJS`. Any invocations will be handled by the browser actor and returned to the requesting host via NATS. Users can pass callbacks to handle invocation and event data.
@ -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 HTTP Server capability into a wasmCloud Host running on a machine
* Load an the wasmcloud-js host into a web browser * Load an the wasmcloud-js host into a web browser
* Load an 'echo' actor into the web browser * Load an 'echo' actor into the web browser
* **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 * Access the webserver, which in turn delivers the request to the actor, processes it, and returns it to the requestion client via the capability
* Unload the actor * Unload the actor
@ -20,6 +17,7 @@ https://user-images.githubusercontent.com/1530656/130013412-b9a9daa6-fc71-424b-8
## Prerequisities ## Prerequisities
* NATS with WebSockets enabled * NATS with WebSockets enabled
@ -47,107 +45,50 @@ $ npm install @wasmcloud/wasmcloud-js
## Usage ## Usage
More examples can be found in the [examples](examples/) directory, including sample `webpack` and `esbuild` configurations
**Browser** **Browser**
```html ```html
<script src="https://unpkg.com/@wasmcloud/wasmcloud-js@<VERSION>/dist/wasmcloud.js"></script> <script src="dist/index.bundle.js"></script>
<script> <script>
(async () => { (async () => {
// Start the host passing the name, registry tls enabled, a list of nats ws/wss hosts or the natsConnection object, and an optional host heartbeat interval (default is 30 seconds) // start the host passing the name, registry tls enabled, a list of nats ws/wss hosts or the natsConnection object, 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); 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 // the host will automatically listen for actors start & stop messages, to manually listen for these messages
// only call these methods if your host is not listening for actor start/stop
// actor invocations are automatically returned to the host. if a user wants to handle the data, they can pass a map of callbacks using the actor ref/wasm file name as the key with a callback(data, result) function. The data contains the invocation data and the result contains the invocation result // actor invocations are automatically returned to the host. if a user wants to handle the data, they can pass a map of callbacks using the actor ref/wasm file name as the key with a callback(data, result) function. The data contains the invocation data and the result contains the invocation result
// (async() => { (async() => {
// await host.listenLaunchActor( await host.listenLaunchActor(
// { {
// "localhost:5000/echo:0.2.2": (data, result) => console.log(data.operation, result); "localhost:5000/echo:0.2.2": (data, result) => console.log(data.operation, result);
// } }
// ); );
// await host.listenStopActor(); await host.listenStopActor();
// })(); })();
// To launch an actor manually from the library from a registry, optionally a callback can be passed to handle the invocation results. In addition, a hostCall callback and writer can be passed. // to launch an actor manually from the library from a registry
// The hostCallback format is as follows: await host.launchActor("registry.com/actor:0.1.1")
// ``` // to launch an actrom manually from local disk (note the .wasm is required)
// (binding, namespace, operation, payload) => {
// return Uint8Array(payload);
// })
// ```
await host.launchActor("registry.com/actor:0.1.1", (data) => { /* handle data */})
// Launch an actor with the hostCallback
await host.launchActor("registry.com/actor:0.1.1", (data) => { /* handle data */}, (binding, namespace, operation, payload) => {
// decode payload via messagepack
// const decoded = decode(payload);
return new Uint8Array(payload);
})
// To launch an actrom manually from local disk (note the .wasm is required)
await host.launchActor("./actor.wasm"); await host.launchActor("./actor.wasm");
// To listen for events, you can call the subscribeToEvents and pass an optional callback to handle the event data // to listen for events, you can call the subscribeToEvents and pass an optional callback to handle the event data
await host.subscribeToEvents((eventData) => console.log(eventData, eventData.source)); await host.subscribeToEvents((eventData) => console.log(eventData, eventData.source));
// To unsubscribe, call the unsubscribeEvents // to unsubscribe, call the unsubscribeEvents
await host.unsubscribeEvents(); await host.unsubscribeEvents();
// To start & stop the heartbeat events // to start & stop the heartbeat events
await host.startHeartbeat(); await host.startHeartbeat(heartbeatInterval?);
await host.stopHeartbeat(); await host.stopHeartbeat();
// The host will automatically connect to nats on start. to connect/reconnect to nats // the host will automatically connect to nats on start. to connect/reconnect to nats
await host.connectNATS(); await host.connectNATS(["ws://locahost:4222/"], {});
// To close/drain all connections from nats, call the disconnectNATS() method // to close/drain all connections from nats, call the disconnectNATS() method
await host.disconnectNATS(); await host.disconnectNATS();
// Stop the host // stop the host
await host.stopHost(); await host.stopHost();
// Restart the host (this only needs to be called if the host is stopped, it is automatically called on the constructor) // restart the host (this only needs to be called if the host is stopped, it is automatically called on the constructor)
await host.startHost(); await host.startHost();
})(); })();
</script> </script>
``` ```
**With a bundler**
There are some caveats to using with a bundler:
* The module contains `.wasm` files that need to be present alongside the final build output. Using `webpack-copy-plugin` (or `fs.copyFile` with other bundlers) can solve this issue.
* If using with `create-react-app`, the webpack config will need to be ejected via `npm run eject` OR an npm library like `react-app-rewired` can handle the config injection.
```javascript
// as esm -- this will grant you access to the types/params
import { startHost } from '@wasmcloud/wasmcloud-js';
// as cjs
// const wasmcloudjs = require('@wasmcloud/wasmcloud-js);
async function cjsHost() {
const host = await wasmcloudjs.startHost('default', false, ['ws://localhost:4222'])
console.log(host);
}
async function esmHost() {
const host = await startHost('default', false, ['ws://localhost:4222'])
console.log(host);
}
cjsHost();
esmHost();
```
```javascript
// webpack config, add this to the plugin section
plugins: [
new CopyPlugin({
patterns: [
{
from: 'node_modules/@wasmcloud/wasmcloud-js/dist/wasmcloud-rs-js/pkg/*.wasm',
to: '[name].wasm'
}
]
}),
]
```
**Node** **Node**
*IN PROGRESS* - NodeJS does not support WebSockets natively (required by nats.ws) In progress--wasm-pack compile issues with nodeJS.
## Contributing ## 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", "name": "@wasmcloud/wasmcloud-js",
"version": "1.0.6", "version": "1.0.0",
"description": "wasmcloud host in JavaScript/Browser", "description": "wasmcloud host in JavaScript/Browser",
"main": "dist/src/index.js", "main": "dist/src/index.js",
"types": "dist/src/index.d.ts", "types": "dist/src/index.d.ts",
"files": [ "files": [
"dist", "dist",
"src", "src",
"README.md", "wasmcloud-rs-js",
"wasmcloud-rs-js" "README.md"
], ],
"scripts": { "scripts": {
"build": "npm run clean && npm run build:browser && npm run build:cjs", "build": "npm run clean && npm run build:browser && npm run build:cjs",
"build:browser": "webpack", "build:browser": "webpack --mode=production",
"build:cjs": "tsc --declaration && webpack --env target=cjs", "build:cjs": "tsc --declaration",
"build:wasm": "cd wasmcloud-rs-js && wasm-pack build", "build:wasm": "cd wasmcloud-rs-js && wasm-pack build",
"clean": "rm -rf ./dist/ && rm -rf ./wasmcloud-rs-js/pkg/", "clean": "rm -rf ./dist/ && rm -rf ./wasmcloud-rs-js/pkg/",
"lint": "eslint --ext .ts src test", "lint": "eslint --ext .ts src test",
"format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'", "format": "prettier --write 'src/**/*.ts' 'test/**/*.ts'",
"test": "mocha --require ts-node/register", "test": "mocha",
"watch": "npm run clean && tsc -w --declrataion", "watch": "npm run clean && tsc -w --declrataion",
"prepare": "npm run build" "prepare": "npm run build"
}, },
"prettier": "./.prettierrc.json", "prettier": ".prettierrc.json",
"keywords": [ "keywords": [
"wasmcloud", "wasmcloud",
"wasmcloud-host", "wasmcloud-host",
"wasmcloud-js",
"wasm" "wasm"
], ],
"eslintConfig": { "eslintConfig": {
@ -46,26 +45,24 @@
"extension": [ "extension": [
"ts" "ts"
], ],
"spec": "test/**/*.test.ts" "spec": "test/**/*.test.ts",
"require": "ts-node/register"
}, },
"author": "ks2211 <kaushik@cosmonic.com>", "author": "ks2211 <kaushik@cosmonic.com>",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.15.0",
"@babel/preset-env": "^7.15.0",
"@types/chai": "^4.2.21", "@types/chai": "^4.2.21",
"@types/chai-as-promised": "^7.1.4", "@types/chai-as-promised": "^7.1.4",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.29.2", "@typescript-eslint/parser": "^4.29.2",
"@wasm-tool/wasm-pack-plugin": "^1.5.0", "@wasm-tool/wasm-pack-plugin": "^1.5.0",
"babel-loader": "^8.2.2",
"chai": "^4.3.4", "chai": "^4.3.4",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.1",
"esbuild": "^0.12.20",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"mocha": "^9.0.3", "mocha": "^9.0.3",
"path": "^0.12.7", "path": "^0.12.7",
"prettier": "^2.3.2",
"ts-loader": "^9.2.5", "ts-loader": "^9.2.5",
"ts-node": "^10.2.1", "ts-node": "^10.2.1",
"typescript": "^4.3.5", "typescript": "^4.3.5",
@ -73,9 +70,9 @@
"webpack-cli": "^4.8.0" "webpack-cli": "^4.8.0"
}, },
"dependencies": { "dependencies": {
"@msgpack/msgpack": "^2.7.1", "@msgpack/msgpack": "^2.7.0",
"@wapc/host": "0.0.2", "@wapc/host": "0.0.2",
"axios": "^0.24.0", "axios": "^0.21.1",
"nats.ws": "^1.2.0" "nats.ws": "^1.2.0"
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 () { describe('wasmcloudjs', function () {
it('should initialize a host with the name and key set', async () => { it('should initialize a host with the name and key set', async () => {
const host = await startHost('default', false, ['ws://localhost:4222']); const host = await startHost('default', false, ["ws://localhost:4222"]);
expect(host.name).to.equal('default'); expect(host.name).to.equal('default');
expect(host.key) expect(host.key).to.be.a('string').and.satisfy((key: string) => key.startsWith('N'));
.to.be.a('string')
.and.satisfy((key: string) => key.startsWith('N'));
expect(host.actors).to.equal({}); 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": [ "include": [
"src/**/*.ts", "src/**/*.ts",
"wasmcloud-rs-js/**/*.ts"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"
@ -24,7 +25,6 @@
"./wasmcloud-rs-js/pkg/" "./wasmcloud-rs-js/pkg/"
], ],
"esModuleInterop": true, "esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true "forceConsistentCasingInFileNames": true
} }
} }

View File

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

View File

@ -1,25 +1,9 @@
const path = require('path'); const path = require('path');
const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin'); const WasmPackPlugin = require('@wasm-tool/wasm-pack-plugin');
const sharedConfig = { const baseConfig = {
stats: { assets: false, modules: false, errors: true }, stats: { assets: false, modules: false },
mode: 'production', mode: 'production',
resolve: {
extensions: ['.tsx', '.ts', '.js', '.wasm']
},
experiments: {
asyncWebAssembly: true
}
}
// this is specifically to use in a script tag
const browserConfig = {
output: {
webassemblyModuleFilename: 'wasmcloud.wasm',
filename: 'wasmcloud.js',
path: path.resolve(__dirname, 'dist'),
library: 'wasmcloudjs'
},
entry: './src/index.ts', entry: './src/index.ts',
module: { module: {
rules: [ rules: [
@ -30,48 +14,44 @@ const browserConfig = {
} }
] ]
}, },
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
plugins: [ plugins: [
new WasmPackPlugin({ new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, 'wasmcloud-rs-js'), crateDirectory: path.resolve(__dirname, 'wasmcloud-rs-js'),
extraArgs: '--target bundler', extraArgs: '--target bundler',
outDir: path.resolve(__dirname, 'wasmcloud-rs-js', 'pkg') outDir: path.resolve(__dirname, 'wasmcloud-rs-js', 'pkg'),
outName: 'wasmcloud_rs_js'
}) })
], ],
...sharedConfig experiments: {
} asyncWebAssembly: true
// 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
} }
}
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];
}; };