Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: WASM package scoping and cleanup error class check #4596

Merged
merged 4 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions wasm/.npmignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
test/
*.md
example.js
developer_readme.md
node_modules
*.txt
*.cc
*.ts
!*.d.ts
22 changes: 9 additions & 13 deletions wasm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@

Javascript bindings for [VowpalWabbit](https://vowpalwabbit.org/)

## Installation

Download the artifact from WASM CI and run `npm install <path to artifact file>`

## Documentation

[API documentation](documentation.md)
Expand All @@ -19,7 +15,7 @@ Full API reference [here](documentation.md#CbWorkspace)
Require returns a promise because we need to wait for the WASM module to be initialized before including and using the VowpalWabbit JS code

```(js)
const vwPromise = require('vowpalwabbit');
const vwPromise = require('@vowpalwabbit/vowpalwabbit');

vwPromise.then((vw) => {
let model = new vw.CbWorkspace({ args_str: "--cb_explore_adf" });
Expand All @@ -32,7 +28,7 @@ A VW model needs to be deleted after we are done with its usage to return the aq
### How-To call learn and predict on a Contextual Bandit model

```(js)
const vwPromise = require('vowpalwabbit');
const vwPromise = require('@vowpalwabbit/vowpalwabbit');

vwPromise.then((vw) => {
let model = new vw.CbWorkspace({ args_str: "--cb_explore_adf" });
Expand Down Expand Up @@ -90,7 +86,7 @@ There are two ways to save/load a model
Node's `fs` will be used to access the file and save/loading is blocking

```(js)
const vwPromise = require('vowpalwabbit');
const vwPromise = require('@vowpalwabbit/vowpalwabbit');

vwPromise.then((vw) => {
let model = new vw.CbWorkspace({ args_str: "--cb_explore_adf" });
Expand Down Expand Up @@ -154,7 +150,7 @@ A model can be loaded from a file either during model construction (shown above)
A log stream can be started which will create and use a `fs` write stream:

```(js)
const vwPromise = require('vowpalwabbit');
const vwPromise = require('@vowpalwabbit/vowpalwabbit');

vwPromise.then((vw) => {

Expand Down Expand Up @@ -186,7 +182,7 @@ Synchronous logging options are also available [see API documentation](documenta
### How-To train a model with data from a file

```(js)
const vwPromise = require('vowpalwabbit');
const vwPromise = require('@vowpalwabbit/vowpalwabbit');

vwPromise.then((vw) => {

Expand All @@ -211,15 +207,15 @@ vwPromise.then((vw) => {

### How-To handle errors

Some function calls with throw if something went wrong or if they were called incorrectly. There are two type of errors that can be thrown: native JavaScript errors and WebAssembly runtime errors.
Some function calls with throw if something went wrong or if they were called incorrectly. There are two type of errors that can be thrown: native JavaScript errors and WebAssembly runtime errors, the latter which are wrapped in a VWError object.

When logging an error to the console there needs to be a check of the error type and the logging needs to be handled accordingly:

```(js)
try {}
catch (e)
{
if (e instanceof WebAssembly.RuntimeError) {
if (e.name === 'VWError') {
console.error(vw.getExceptionMessage(e));
}
else {
Expand All @@ -235,7 +231,7 @@ Full API reference [here](documentation.md#Workspace)
#### Simple regression example

```(js)
const vwPromise = require('vowpalwabbit');
const vwPromise = require('@vowpalwabbit/vowpalwabbit');

vwPromise.then((vw) => {

Expand All @@ -251,7 +247,7 @@ vwPromise.then((vw) => {
#### CCB example

```(js)
const vwPromise = require('vowpalwabbit');
const vwPromise = require('@vowpalwabbit/vowpalwabbit');

vwPromise.then((vw) => {

Expand Down
11 changes: 6 additions & 5 deletions wasm/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vowpalwabbit",
"version": "0.0.1",
"name": "@vowpalwabbit/vowpalwabbit",
"version": "0.0.3",
"description": "wasm bindings for vowpal wabbit",
"exports": {
"require": "./dist/vw.js"
Expand All @@ -18,7 +18,7 @@
"typescript": "^5.0.4"
},
"scripts": {
"postinstall": "npm run build",
"prepublish": "npm run build",
"build": "tsc",
"test": "node --experimental-wasm-threads ./node_modules/mocha/bin/mocha --delay",
"docs": "jsdoc2md ./dist/vw.js > documentation.md"
Expand All @@ -28,7 +28,8 @@
},
"repository": {
"type": "git",
"url": "https://github.com/VowpalWabbit/vowpal_wabbit/wasm"
"url": "https://github.com/VowpalWabbit/vowpal_wabbit.git",
"directory": "wasm"
},
"keywords": [
"vowpal",
Expand All @@ -41,4 +42,4 @@
],
"author": "olgavrou",
"license": "BSD-3-Clause"
}
}
88 changes: 67 additions & 21 deletions wasm/src/vw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ const ProblemType =

// exported

class VWError extends Error {
constructor(message: string, originalError: any) {
super(message);
this.name = 'VWError';
this.stack = originalError;
}
}

/**
* A class that helps facilitate the stringification of Vowpal Wabbit examples, and the logging of Vowpal Wabbit examples to a file.
* @class
Expand Down Expand Up @@ -349,18 +357,28 @@ module.exports = new Promise((resolve) => {
*
* @param {object} example returned from parse()
* @returns the prediction with a type corresponding to the reduction that was used
* @throws {VWError} Throws an error if the example is not well defined
*/
predict(example: object) {
return this._instance.predict(example);
try {
return this._instance.predict(example);
} catch (e: any) {
throw new VWError(e.message, e);
}
}

/**
* Calls vw learn on the example and updates the model
*
* @param {object} example returned from parse()
* @throws {VWError} Throws an error if the example is not well defined
*/
learn(example: object) {
return this._instance.learn(example);
try {
return this._instance.learn(example);
} catch (e: any) {
throw new VWError(e.message, e);
}
}

/**
Expand Down Expand Up @@ -411,9 +429,14 @@ module.exports = new Promise((resolve) => {
*
* @param {object} example the example object that will be used for prediction
* @returns {array} probability mass function, an array of action,score pairs that was returned by predict
* @throws {VWError} Throws an error if the example text_context is missing from the example
*/
predict(example: object) {
return this._instance.predict(example);
try {
return this._instance.predict(example);
} catch (e: any) {
throw new VWError(e.message, e);
}
}

/**
Expand All @@ -430,9 +453,15 @@ module.exports = new Promise((resolve) => {
*
*
* @param {object} example the example object that will be used for prediction
* @throws {VWError} Throws an error if the example does not have the required properties to learn
*/
learn(example: object) {
return this._instance.learn(example);
try {
return this._instance.learn(example);
}
catch (e: any) {
throw new VWError(e.message, e);
}
}

/**
Expand Down Expand Up @@ -480,13 +509,18 @@ module.exports = new Promise((resolve) => {
* - action: the action index that was sampled
* - score: the score of the action that was sampled
* - uuid: the uuid that was passed to the predict function
* @throws {Error} Throws an error if the input is not an array of action,score pairs
* @throws {VWError} Throws an error if the input is not an array of action,score pairs
*/
samplePmf(pmf: Array<number>): object {
let uuid = crypto.randomUUID();
let ret = this._instance._samplePmf(pmf, uuid);
ret["uuid"] = uuid;
return ret;
try {
let ret = this._instance._samplePmf(pmf, uuid);
ret["uuid"] = uuid;
return ret;
}
catch (e: any) {
throw new VWError(e.message, e);
}
}

/**
Expand All @@ -500,12 +534,16 @@ module.exports = new Promise((resolve) => {
* - action: the action index that was sampled
* - score: the score of the action that was sampled
* - uuid: the uuid that was passed to the predict function
* @throws {Error} Throws an error if the input is not an array of action,score pairs
* @throws {VWError} Throws an error if the input is not an array of action,score pairs
*/
samplePmfWithUUID(pmf: Array<number>, uuid: string): object {
let ret = this._instance._samplePmf(pmf, uuid);
ret["uuid"] = uuid;
return ret;
try {
let ret = this._instance._samplePmf(pmf, uuid);
ret["uuid"] = uuid;
return ret;
} catch (e: any) {
throw new VWError(e.message, e);
}
}

/**
Expand All @@ -519,13 +557,17 @@ module.exports = new Promise((resolve) => {
* - action: the action index that was sampled
* - score: the score of the action that was sampled
* - uuid: the uuid that was passed to the predict function
* @throws {Error} if there is no text_context field in the example
* @throws {VWError} if there is no text_context field in the example
*/
predictAndSample(example: object): object {
let uuid = crypto.randomUUID();
let ret = this._instance._predictAndSample(example, uuid);
ret["uuid"] = uuid;
return ret;
try {
let uuid = crypto.randomUUID();
let ret = this._instance._predictAndSample(example, uuid);
ret["uuid"] = uuid;
return ret;
} catch (e: any) {
throw new VWError(e.message, e);
}
}

/**
Expand All @@ -539,12 +581,16 @@ module.exports = new Promise((resolve) => {
* - action: the action index that was sampled
* - score: the score of the action that was sampled
* - uuid: the uuid that was passed to the predict function
* @throws {Error} if there is no text_context field in the example
* @throws {VWError} if there is no text_context field in the example
*/
predictAndSampleWithUUID(example: object, uuid: string): object {
let ret = this._instance._predictAndSample(example, uuid);
ret["uuid"] = uuid;
return ret;
try {
let ret = this._instance._predictAndSample(example, uuid);
ret["uuid"] = uuid;
return ret;
} catch (e: any) {
throw new VWError(e.message, e);
}
}
};

Expand Down
32 changes: 29 additions & 3 deletions wasm/test/example.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const vwPromise = require('vowpalwabbit');
const vwPromise = require('@vowpalwabbit/vowpalwabbit');
const fs = require('fs');

// Delay test execution until the WASM VWModule is ready
Expand Down Expand Up @@ -95,9 +95,35 @@ vwPromise.then((vw) => {
catch (e) {
// Exceptions that are produced by the module must be passed through
// this transformation function to get the error info.
if (e instanceof WebAssembly.RuntimeError) {
console.error(vw.getExceptionMessage(e));
if (e.name === 'VWError') {
console.error(vw.getExceptionMessage(e.stack));
}
else { console.error(e); }
}
}).catch(err => { console.log(err) });

vwPromise.then((vw) => {
try {
let model = new vw.CbWorkspace({ args_str: "--cb_explore_adf" });

let example = {
text_context: `shared | s_1 s_2
| a_1 b_1 c_1
| a_2 b_2 c_2
| a_3 b_3 c_3`,
};

// model learn without a label should throw
model.learn(example);

model.delete();
}
catch (e) {
// Exceptions that are produced by the module must be passed through
// this transformation function to get the error info.
if (e.name === 'VWError') {
console.error(vw.getExceptionMessage(e.stack));
}
else { console.error(e); }
}
}).catch(err => { console.log(err) });
2 changes: 1 addition & 1 deletion wasm/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const fs = require('fs');
const readline = require('readline');
const path = require('path');

const vwPromise = require('vowpalwabbit');
const vwPromise = require('@vowpalwabbit/vowpalwabbit');
let vw;

async function run() {
Expand Down