Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
536 changes: 536 additions & 0 deletions docs/hot-reload.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion engine/baml-runtime/src/tracingv2/publisher/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ pub fn start_publisher(
return;
}
if lookup.env_var("BOUNDARY_API_KEY").is_none() {
log::debug!("Skipping publisher because BOUNDARY_API_KEY is not set");
log::debug!("Skipping publisher because BOUNDARY_API_KEY is not set---ahh");
return;
}
log::debug!("Starting publisher");
Expand Down
5 changes: 5 additions & 0 deletions engine/baml-schema-wasm/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
dist/src/*
nodejs/src/*
web/src/*

# Hot-reload temporary files
web/.wasm-build.lock
web/.wasm-build-status
web/.wasm-build-output.tmp
139 changes: 139 additions & 0 deletions engine/baml-schema-wasm/bacon.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
# This is a configuration file for the bacon tool
#
# Complete help on configuration: https://dystroy.org/bacon/config/
#
# You may check the current default at
# https://github.com/Canop/bacon/blob/main/defaults/default-bacon.toml

default_job = "check"
env.CARGO_TERM_COLOR = "always"

[jobs.check]
command = ["cargo", "check"]
need_stdout = false

[jobs.check-all]
command = ["cargo", "check", "--all-targets"]
need_stdout = false

# Run clippy on the default target
[jobs.clippy]
command = ["cargo", "clippy"]
need_stdout = false

# Run clippy on all targets
# To disable some lints, you may change the job this way:
# [jobs.clippy-all]
# command = [
# "cargo", "clippy",
# "--all-targets",
# "--",
# "-A", "clippy::bool_to_int_with_if",
# "-A", "clippy::collapsible_if",
# "-A", "clippy::derive_partial_eq_without_eq",
# ]
# need_stdout = false
[jobs.clippy-all]
command = ["cargo", "clippy", "--all-targets"]
need_stdout = false

# This job lets you run
# - all tests: bacon test
# - a specific test: bacon test -- config::test_default_files
# - the tests of a package: bacon test -- -- -p config
[jobs.test]
command = ["cargo", "test"]
need_stdout = true

[jobs.nextest]
command = [
"cargo", "nextest", "run",
"--hide-progress-bar", "--failure-output", "final"
]
need_stdout = true
analyzer = "nextest"

[jobs.doc]
command = ["cargo", "doc", "--no-deps"]
need_stdout = false

# If the doc compiles, then it opens in your browser and bacon switches
# to the previous job
[jobs.doc-open]
command = ["cargo", "doc", "--no-deps", "--open"]
need_stdout = false
on_success = "back" # so that we don't open the browser at each change

# You can run your application and have the result displayed in bacon,
# if it makes sense for this crate.
[jobs.run]
command = [
"cargo", "run",
# put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
background = true

# Run your long-running application (eg server) and have the result displayed in bacon.
# For programs that never stop (eg a server), `background` is set to false
# to have the cargo run output immediately displayed instead of waiting for
# program's end.
# 'on_change_strategy' is set to `kill_then_restart` to have your program restart
# on every change (an alternative would be to use the 'F5' key manually in bacon).
# If you often use this job, it makes sense to override the 'r' key by adding
# a binding `r = job:run-long` at the end of this file .
# A custom kill command such as the one suggested below is frequently needed to kill
# long running programs (uncomment it if you need it)
[jobs.run-long]
command = [
"cargo", "run",
# put launch parameters for your program behind a `--` separator
]
need_stdout = true
allow_warnings = true
background = false
on_change_strategy = "kill_then_restart"
# kill = ["pkill", "-TERM", "-P"]

# [jobs.build-wasm]
# command = [
# # "sh", "-c", "sleep 1 && exec cargo run --bin http-server",
# "pnpm build",
# # put launch parameters for your program behind a `--` separator
# ]
# need_stdout = true
# allow_warnings = true
# background = false
# on_change_strategy = "kill_then_restart"
# watch = ["rust-src"]
# ignore = ["baml/engine/baml-rpc/bindings"]

# Hot-reload development job for Vite playground
# Runs watch-build.py script to build WASM with file locking
[jobs.watch-wasm-dev]
command = ["./watch-build.py"]
need_stdout = true
allow_warnings = true
background = false
on_change_strategy = "kill_then_restart"
watch = ["src"]
ignore = ["web/dist", "target"]


# This parameterized job runs the example of your choice, as soon
# as the code compiles.
# Call it as
# bacon ex -- my-example
[jobs.ex]
command = ["cargo", "run", "--example"]
need_stdout = true
allow_warnings = true

# You may define here keybindings that would be specific to
# a project, for example a shortcut to launch a specific job.
# Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead.
[keybindings]
# alt-m = "job:my-job"
c = "job:clippy-all" # comment this to have 'c' run clippy on only the default target
2 changes: 1 addition & 1 deletion engine/baml-schema-wasm/src/abort_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use baml_runtime::TripWire;
use stream_cancel::Trigger;
use wasm_bindgen::prelude::*;
use web_sys::AbortSignal;

// ...
thread_local! {
static ABORT_CLOSURES: RefCell<HashMap<u32, Closure<dyn Fn()>>> = RefCell::new(HashMap::new());
static OPERATION_TRIGGERS: RefCell<HashMap<u32, Trigger>> = RefCell::new(HashMap::new());
Expand Down
5 changes: 3 additions & 2 deletions engine/baml-schema-wasm/src/runtime_wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub fn on_wasm_init() {
// this is disabled by default because its slows down release mode builds.
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {

const LOG_LEVEL: log::Level = log::Level::Debug;
} else {
const LOG_LEVEL: log::Level = log::Level::Debug;
Expand All @@ -71,10 +72,10 @@ pub fn on_wasm_init() {
//wasm_logger::init(wasm_logger::Config::new(LOG_LEVEL));
match console_log::init_with_level(LOG_LEVEL) {
Ok(_) => web_sys::console::log_1(
&format!("Initialized BAML runtime logging as log::{LOG_LEVEL}").into(),
&format!("Initialized BAML runtime logging as log::{LOG_LEVEL}...").into(),
),
Err(e) => web_sys::console::log_1(
&format!("Failed to initialize BAML runtime logging: {e:?}").into(),
&format!("Failed to initialize BAML runtime logging: {e:?}.").into(),
),
}

Expand Down
153 changes: 153 additions & 0 deletions engine/baml-schema-wasm/watch-build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
#!/usr/bin/env -S uv run --quiet --script
# /// script
# requires-python = ">=3.11"
# ///
"""
Hot-reload build script for WASM development.
Called by bacon when Rust source files change.
"""

import fcntl
import os
import subprocess
import sys
import signal
from pathlib import Path

# Configuration
SCRIPT_DIR = Path(__file__).parent
LOCK_FILE = SCRIPT_DIR / "web" / ".wasm-build.lock"
STATUS_FILE = SCRIPT_DIR / "web" / ".wasm-build-status"
OUTPUT_FILE = SCRIPT_DIR / "web" / ".wasm-build-output.tmp"

# Global state for cleanup
lock_fd = None
should_cleanup = True


def write_status(status: str):
"""Write status and flush immediately."""
STATUS_FILE.write_text(status)
# Force filesystem sync
os.sync()


def cleanup(signum=None, frame=None):
"""Clean up resources on exit."""
global should_cleanup

if not should_cleanup:
return

print("Cleaning up...", file=sys.stderr)

# If interrupted while refreshing, mark as cancelled
if STATUS_FILE.exists() and STATUS_FILE.read_text().strip() == "refreshing":
write_status("cancelled")
print("Status: Build cancelled", file=sys.stderr)

# Remove temp files
if OUTPUT_FILE.exists():
OUTPUT_FILE.unlink()

# Release lock (will happen automatically, but being explicit)
if lock_fd is not None:
try:
fcntl.flock(lock_fd, fcntl.LOCK_UN)
os.close(lock_fd)
except:
pass

print("Cleanup complete", file=sys.stderr)
should_cleanup = False


def main():
global lock_fd

# Change to script directory
os.chdir(SCRIPT_DIR)

# Set up signal handlers
signal.signal(signal.SIGINT, cleanup)
signal.signal(signal.SIGTERM, cleanup)

# Create lock file if it doesn't exist
LOCK_FILE.parent.mkdir(parents=True, exist_ok=True)
LOCK_FILE.touch()

# Try to acquire exclusive lock (non-blocking)
try:
lock_fd = os.open(str(LOCK_FILE), os.O_RDWR | os.O_CREAT)
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
print("Another build is in progress, skipping...", file=sys.stderr)
return 0

print("Lock acquired, running build...", file=sys.stderr)

# Signal that build is starting
write_status("refreshing")
print("Status: Build starting (refreshing)", file=sys.stderr)

# Run wasm-pack build
# Note: Using --release even for dev because --dev bundles are too slow
cmd = [
"wasm-pack",
"build",
"./",
"--target",
"bundler",
"--out-dir",
"./web/dist",
# cant use --dev until we solve this issue: https://github.com/wasm-bindgen/wasm-bindgen/issues/1563
"--dev",
]

# Run process and capture output while displaying it
with open(OUTPUT_FILE, "w") as output_f:
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1
)

# Stream output to both stdout and file
for line in process.stdout:
print(line, end="")
output_f.write(line)

process.wait()
exit_code = process.returncode

print(f"Build exit code: {exit_code}", file=sys.stderr)

# Write status based on exit code
if exit_code == 0:
write_status("success")
print("=" * 47, file=sys.stderr)
print("Status: Build succeeded (success written to file)", file=sys.stderr)
print("=" * 47, file=sys.stderr)

# Verify
verify_status = STATUS_FILE.read_text().strip()
print(f"Verified: status file contains: '{verify_status}'", file=sys.stderr)
else:
# Build failed - save output for Vite error overlay
error_output = OUTPUT_FILE.read_text()
write_status(error_output)
print("=" * 47, file=sys.stderr)
print("Status: Build failed (errors written)", file=sys.stderr)
print("=" * 47, file=sys.stderr)

# Clean up
cleanup()

return exit_code


if __name__ == "__main__":
try:
sys.exit(main())
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
cleanup()
sys.exit(1)
Loading
Loading