From 1a360b7673de3bafef7ba089cd482c5326c5378c Mon Sep 17 00:00:00 2001 From: Ben Sully Date: Wed, 16 Oct 2024 15:49:32 +0100 Subject: [PATCH] fix: fix several issues with JS bindings (#131) --- .github/workflows/wasmstan.yml | 2 +- components/js/prophet-wasmstan/README.md | 21 +++++++++++++++ components/js/prophet-wasmstan/run.js | 17 ++++++++++++ components/justfile | 22 +++++++--------- crates/augurs-js/README.md | 28 +++++++++++++++++++- crates/augurs-js/src/prophet.rs | 29 ++++++--------------- crates/augurs-prophet/src/positive_float.rs | 13 ++++++++- 7 files changed, 95 insertions(+), 37 deletions(-) create mode 100644 components/js/prophet-wasmstan/run.js diff --git a/.github/workflows/wasmstan.yml b/.github/workflows/wasmstan.yml index b2913ffa..186ff932 100644 --- a/.github/workflows/wasmstan.yml +++ b/.github/workflows/wasmstan.yml @@ -20,7 +20,7 @@ jobs: targets: wasm32-unknown-unknown,wasm32-wasip1 - uses: taiki-e/install-action@v2 with: - tool: cargo-binstall,just,wasmtime + tool: cargo-binstall,just,ripgrep,wasmtime - name: Install deps run: just components/install-deps - uses: actions/setup-node@v4 diff --git a/components/js/prophet-wasmstan/README.md b/components/js/prophet-wasmstan/README.md index 5d7f6e72..8418145a 100644 --- a/components/js/prophet-wasmstan/README.md +++ b/components/js/prophet-wasmstan/README.md @@ -26,3 +26,24 @@ prophet.predict({ ds: [ 1713717414 ]}) ``` See the documentation for `@bsull/augurs` for more details. + +## Troubleshooting + +### Webpack + +The generated Javascript bindings in this package may require some additional Webpack configuration to work. +Adding this to your `webpack.config.js` should be enough: + +```javascript +{ + experiments: { + // Required to load WASM modules. + asyncWebAssembly: true, + }, + resolve: { + fallback: { + fs: false, + }, + }, +} +``` diff --git a/components/js/prophet-wasmstan/run.js b/components/js/prophet-wasmstan/run.js new file mode 100644 index 00000000..181107d5 --- /dev/null +++ b/components/js/prophet-wasmstan/run.js @@ -0,0 +1,17 @@ +// Note: this function comes from https://stackoverflow.com/questions/30401486/ecma6-generators-yield-promise. +// It is used to convert a generator function into a promise. +// `jco transpile` generates a similar function but it didn't work for me. +// I'm not sure why, but I'll raise an issue on the `jco` repo. +// See the `justfile` for how this gets shimmed into the transpiled code; +// in short, we use `ripgrep` as in +// https://unix.stackexchange.com/questions/181180/replace-multiline-string-in-files +// (it was a Stack-Overflow heavy day...) +// The indentation is intentional so the function matches the original. + function run(g) { + return Promise.resolve(function step(v) { + const res = g.next(v); + if (res.done) return res.value; + return res.value.then(step); + }()); + } + return run(gen); diff --git a/components/justfile b/components/justfile index 0eb20289..23fc3109 100644 --- a/components/justfile +++ b/components/justfile @@ -74,21 +74,17 @@ transpile: build --name prophet-wasmstan \ --out-dir js/prophet-wasmstan \ cpp/prophet-wasmstan/wit/prophet-wasmstan.wit - -transpile-min: build - jco transpile \ - --name prophet-wasmstan \ - --minify \ - --optimize \ - --out-dir js/prophet-wasmstan \ - cpp/prophet-wasmstan/prophet-wasmstan-component.wasm - jco types \ - --name prophet-wasmstan \ - --out-dir js/prophet-wasmstan \ - cpp/prophet-wasmstan/wit/prophet-wasmstan.wit + rg --replace="$(rg --invert-match --no-line-number '//' js/prophet-wasmstan/run.js)" \ + --multiline --multiline-dotall \ + --passthru \ + --no-line-number \ + ' let promise, resolve, reject;.+?return promise \|\| maybeSyncReturn;' \ + js/prophet-wasmstan/prophet-wasmstan.js \ + > js/prophet-wasmstan/prophet-wasmstan.fixed.js + mv js/prophet-wasmstan/prophet-wasmstan.fixed.js js/prophet-wasmstan/prophet-wasmstan.js test: transpile cd js/prophet-wasmstan && npm ci && npm run test:ci -publish: transpile-min +publish: transpile cd js/prophet-wasmstan && npm ci && npm publish --access public diff --git a/crates/augurs-js/README.md b/crates/augurs-js/README.md index 6b9e885e..449cdc3a 100644 --- a/crates/augurs-js/README.md +++ b/crates/augurs-js/README.md @@ -8,7 +8,7 @@ Javascript bindings to the [`augurs`][repo] time series framework. ```json "dependencies": { - "@bsull/augurs": "^0.3.0" + "@bsull/augurs": "^0.4.1" } ``` @@ -36,4 +36,30 @@ const { point, lower, upper } = model.predictInSample(predictionInterval); const { point: futurePoint, lower: futureLower, upper: futureUpper } = model.predict(10, predictionInterval); ``` +## Troubleshooting + +### Webpack + +Some of the dependencies of `augurs` require a few changes to the Webpack configuration to work correctly. +Adding this to your `webpack.config.js` should be enough: + +```javascript +{ + experiments: { + // Required to load WASM modules. + asyncWebAssembly: true, + }, + module: { + rules: [ + { + test: /\@bsull\/augurs\/.*\.js$/, + resolve: { + fullySpecified: false + } + }, + ] + }, +} +``` + [repo]: https://github.com/grafana/augurs diff --git a/crates/augurs-js/src/prophet.rs b/crates/augurs-js/src/prophet.rs index 20104774..baae8bac 100644 --- a/crates/augurs-js/src/prophet.rs +++ b/crates/augurs-js/src/prophet.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, mem, num::TryFromIntError}; +use std::{collections::HashMap, num::TryFromIntError}; use augurs_prophet::PositiveFloat; use js_sys::{Float64Array, Int32Array}; @@ -414,28 +414,14 @@ struct Logs { pub error: String, /// Fatal logs. pub fatal: String, - - #[serde(default, skip)] - emitted_header: bool, } impl Logs { - fn emit(mut self) { - let debug = mem::take(&mut self.debug); - let info = mem::take(&mut self.info); - let warn = mem::take(&mut self.warn); - let error = mem::take(&mut self.error); - let fatal = mem::take(&mut self.fatal); - for line in debug.lines() { + fn emit(self) { + for line in self.debug.lines() { tracing::trace!(target: "augurs::prophet::stan::optimize", "{}", line); } - for line in info.lines() { - if line.contains("Iter") { - if self.emitted_header { - return; - } - self.emitted_header = true; - } + for line in self.info.lines().filter(|line| !line.contains("Iter")) { match ConvergenceLog::new(line) { Some(log) => { tracing::debug!( @@ -455,13 +441,13 @@ impl Logs { } } } - for line in warn.lines() { + for line in self.warn.lines() { tracing::warn!(target: "augurs::prophet::stan::optimize", "{}", line); } - for line in error.lines() { + for line in self.error.lines() { tracing::error!(target: "augurs::prophet::stan::optimize", "{}", line); } - for line in fatal.lines() { + for line in self.fatal.lines() { tracing::error!(target: "augurs::prophet::stan::optimize", "{}", line); } } @@ -488,6 +474,7 @@ struct OptimizedParams { /// Trend offset. pub m: f64, /// Observation noise. + #[tsify(type = "number")] pub sigma_obs: PositiveFloat, /// Trend rate adjustments. #[tsify(type = "Float64Array")] diff --git a/crates/augurs-prophet/src/positive_float.rs b/crates/augurs-prophet/src/positive_float.rs index fb65f19f..b6aed6ec 100644 --- a/crates/augurs-prophet/src/positive_float.rs +++ b/crates/augurs-prophet/src/positive_float.rs @@ -2,7 +2,7 @@ #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] #[cfg_attr(feature = "bytemuck", derive(bytemuck::Pod, bytemuck::Zeroable))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct PositiveFloat(f64); /// An invalid float was provided when trying to create a [`PositiveFloat`]. @@ -48,3 +48,14 @@ impl From for f64 { value.0 } } + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for PositiveFloat { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let f = f64::deserialize(deserializer)?; + Self::try_new(f).map_err(serde::de::Error::custom) + } +}