diff --git a/wasm/.npmignore b/wasm/.npmignore index 0f6b9c51162..e0d23901300 100644 --- a/wasm/.npmignore +++ b/wasm/.npmignore @@ -1,8 +1,5 @@ test/ -*.md -example.js +developer_readme.md node_modules *.txt *.cc -*.ts -!*.d.ts diff --git a/wasm/README.md b/wasm/README.md index cf7fe1882fa..c361a3a7580 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -2,10 +2,6 @@ Javascript bindings for [VowpalWabbit](https://vowpalwabbit.org/) -## Installation - -Download the artifact from WASM CI and run `npm install ` - ## Documentation [API documentation](documentation.md) @@ -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" }); @@ -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" }); @@ -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" }); @@ -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) => { @@ -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) => { @@ -211,7 +207,7 @@ 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: @@ -219,7 +215,7 @@ When logging an error to the console there needs to be a check of the error type try {} catch (e) { - if (e instanceof WebAssembly.RuntimeError) { + if (e.name === 'VWError') { console.error(vw.getExceptionMessage(e)); } else { @@ -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) => { @@ -251,7 +247,7 @@ vwPromise.then((vw) => { #### CCB example ```(js) -const vwPromise = require('vowpalwabbit'); +const vwPromise = require('@vowpalwabbit/vowpalwabbit'); vwPromise.then((vw) => { diff --git a/wasm/package.json b/wasm/package.json index 5828228cd65..8525908f142 100644 --- a/wasm/package.json +++ b/wasm/package.json @@ -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" @@ -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" @@ -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", @@ -41,4 +42,4 @@ ], "author": "olgavrou", "license": "BSD-3-Clause" -} \ No newline at end of file +} diff --git a/wasm/src/vw.ts b/wasm/src/vw.ts index 7470cb9c683..0bb8fa62e39 100644 --- a/wasm/src/vw.ts +++ b/wasm/src/vw.ts @@ -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 @@ -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); + } } /** @@ -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); + } } /** @@ -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); + } } /** @@ -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): 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); + } } /** @@ -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, 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); + } } /** @@ -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); + } } /** @@ -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); + } } }; diff --git a/wasm/test/example.js b/wasm/test/example.js index db8577d728d..02c2a5dbc27 100644 --- a/wasm/test/example.js +++ b/wasm/test/example.js @@ -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 @@ -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) }); \ No newline at end of file diff --git a/wasm/test/test.js b/wasm/test/test.js index 16b6adaae5f..ba344a1b69c 100644 --- a/wasm/test/test.js +++ b/wasm/test/test.js @@ -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() {