Skip to content

Implement AlloyScript SQLite runtime#7

Draft
yumin-chen wants to merge 17 commits intoAlloyScript/initfrom
feat/alloyscript-sqlite-runtime-12892646626400089077
Draft

Implement AlloyScript SQLite runtime#7
yumin-chen wants to merge 17 commits intoAlloyScript/initfrom
feat/alloyscript-sqlite-runtime-12892646626400089077

Conversation

@yumin-chen
Copy link
Copy Markdown
Owner

Designed and implemented the AlloyScript SQLite runtime, providing a high-performance, synchronous SQLite3 driver to the WebView. The implementation includes:

  • A C++ sqlite_runtime class wrapping SQLite3 with statement caching and parameter binding.
  • A robust JavaScript-to-C++ bridge that handles BigInt (via string suffixing) and BLOB (via Base64) data types.
  • A JavaScript API mirroring better-sqlite3, featuring transactions, prepared statements, and result-to-class mapping.
  • Support for lazy iteration over large result sets using JavaScript generators and native sqlite3_step.
  • Integration into the CMake build system with SQLite3 as a required dependency.
  • A new test suite covering database operations, parameter binding, strict mode, and BigInt support.

PR created automatically by Jules for task 12892646626400089077 started by @yumin-chen

google-labs-jules bot and others added 5 commits March 29, 2026 15:48
This commit introduces a new AlloyScript runtime integrated into the
webview library. Key features include:
- `window.Alloy.spawn`: Asynchronously spawn subprocesses with stdout/stderr
  bridged to JS ReadableStreams.
- `window.Alloy.spawnSync`: Synchronously spawn processes and capture output.
- `window.Alloy.Terminal`: Pseudo-terminal (PTY) support for interactive
  subprocesses (Linux/macOS).
- Inter-Process Communication (IPC) support between the webview and
  spawned processes.
- Thread-safe callbacks and non-blocking I/O for `stdin` writing.
- Portable implementation using `posix_spawn` and `poll`.

Added an example in `examples/alloy_test.cc` to demonstrate these features.

Co-authored-by: yumin-chen <10954839+yumin-chen@users.noreply.github.com>
This commit introduces the AlloyScript runtime and Shell scripting API
integrated into the webview library. Key features include:

- `window.Alloy.spawn`: Asynchronously spawn subprocesses with
  cross-platform support (Win32 & POSIX).
- `window.Alloy.$`: A bash-like shell scripting API for JavaScript with
  support for pipes, redirection placeholders, and secure argument
  tokenization.
- Pseudo-terminal (PTY) support via `window.Alloy.Terminal` (POSIX).
- Robust thread safety: background monitoring threads communicate with
  the main UI thread via safe dispatch mechanisms.
- Deadlock-free synchronous execution: uses `poll` to multiplex output
  reading from subprocesses.
- Ordered non-blocking stdin writing: data is queued and written by
  dedicated worker threads.
- Comprehensive JS API: includes fluent methods like `.text()`,
  `.json()`, `.lines()`, and global configuration for `cwd`, `env`,
  and error behavior.

Added examples `examples/alloy_test.cc` and `examples/alloy_shell_test.cc`
to demonstrate the new capabilities.

Co-authored-by: yumin-chen <10954839+yumin-chen@users.noreply.github.com>
This commit completes the AlloyScript runtime integration for the
webview library. Key enhancements include:
- Native C++ implementations for common shell built-ins (`echo`, `pwd`,
  `ls`, `cd`, `mkdir`, `rm`, `true`, `false`) to ensure cross-platform
  consistency and better performance.
- Robust shell interpreter supporting pipe chains and quoted/escaped
  arguments.
- Complete cross-platform process management (Win32 & POSIX).
- Thread-safe background monitoring with safe dispatch to the UI thread.
- Efficient, non-blocking stdin writing via background worker queues.
- Comprehensive JavaScript API via `window.Alloy.$`, providing fluent
  result processing and secure interpolation.

Added `examples/alloy_shell_test.cc` to exercise the full suite of
new shell features.

Co-authored-by: yumin-chen <10954839+yumin-chen@users.noreply.github.com>
…lt-ins

This commit introduces a robust AlloyScript runtime integrated into the
webview library, following a host-orchestrator architecture.

Key features include:
- window.Alloy.spawn (Async) and window.Alloy.spawnSync (Sync) for
  cross-platform subprocess management (Win32 & POSIX).
- window.Alloy.$ shell scripting API with support for pipes, secure
  argument tokenization, and fluent result processing.
- Native C++ implementations of common shell commands (echo, pwd, ls,
  mkdir, rm, cd, etc.) for cross-platform consistency.
- Thread-safe background monitoring with safe dispatch to the UI thread.
- Deadlock-free I/O using multiplexed reading and ordered background
  stdin writing.
- PTY support for interactive terminals on POSIX systems.

Added comprehensive unit tests in `core/tests/src/alloy_tests.cc`
covering:
- Shell tokenization (quotes and escapes).
- Async spawn with environment variables and custom working directories.
- spawnSync with output capturing.
- Complex shell pipe chains.

Demonstration examples included in `examples/alloy_shell_test.cc`.

Co-authored-by: yumin-chen <10954839+yumin-chen@users.noreply.github.com>
- Added `sqlite_runtime.hh` with SQLite3 C++ wrappers for Database and Statement.
- Integrated `sqlite_runtime` into `engine_base` and added native C++ bindings for WebView communication.
- Updated `create_alloy_script` to inject JavaScript `Database` and `Statement` classes and an import map for `Alloy:sqlite`.
- Implemented high-performance BigInt and BLOB handling using string serialization and Base64.
- Added true lazy `iterate()` support via C++ `step` binding and JavaScript generators.
- Implemented `loadExtension()` and static `Database.deserialize()`.
- Added SQLite3 as a project dependency in `cmake/webview.cmake`.
- Added comprehensive unit tests in `core/tests/src/sqlite_tests.cc`.
@google-labs-jules
Copy link
Copy Markdown

👋 Jules, reporting for duty! I'm here to lend a hand with this pull request.

When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down.

I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job!

For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with @jules. You can find this option in the Pull Request section of your global Jules UI settings. You can always switch back!

New to Jules? Learn more at jules.google/docs.


For security, I will only act on instructions from the user who triggered this task.

- Supported both `Alloy:sqlite` and `alloy:sqlite` imports in the import map.
- Added `Database.open()` and `Database.exec()` JavaScript methods.
- Implemented `Alloy_sqlite_exec` C++ binding using `sqlite3_exec` for multi-query support.
- Updated `Database.run()` to use `Alloy_sqlite_exec` when no parameters are provided.
- Improved BigInt and BLOB handling via string 'n' suffix and Base64.
- Added true lazy iteration support using generators and native `step`.
- Added test cases for multi-query execution.
…zle compatibility

- Enhanced `sqlite_runtime.hh` to provide `columnNames`, `declaredTypes`, `columnTypes`, and `paramsCount` metadata.
- Improved BigInt support to return all integers as BigInt when `safeIntegers` is enabled.
- Refined JavaScript `Statement` class to lazily fetch metadata and update `columnTypes` after execution.
- Added support for `[Symbol.dispose]` in `Database` and `Statement` for the `using` statement.
- Exposed SQLite constants in the `Alloy:sqlite` module.
- Supported both `Alloy:sqlite` and `alloy:sqlite` module imports.
- Added `Database.open()` and `Database.setCustomSQLite()` static methods.
- Verified all functionality with an expanded test suite.
- Removed WebView project license from AlloyScript-specific `sqlite_runtime.hh`.
- Implemented high-performance synchronous SQLite3 driver in C++.
- Created robust JavaScript-to-C++ bridge supporting BigInt, BLOBs, and lazy iteration.
- Initialized project as a Bun project named `@alloyscript/runtime`.
- Added `scripts/build_executable.ts` to transpile AlloyScript and embed it into a C++ host program.
- Added comprehensive `bun:test` suite for SQLite functionality.
- Supported `Alloy:sqlite` and `alloy:sqlite` modules with Drizzle ORM compatibility.
- Verified all features with both C++ unit tests and Bun tests.
Repository owner deleted a comment from google-labs-jules bot Mar 29, 2026
@google-labs-jules
Copy link
Copy Markdown

eval is another global object, not a property of window https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval

You're right, eval is a global object. I have updated the implementation to bind a native Alloy_eval to the bridge and exposed it via Alloy.eval, ensuring it bypasses any restrictive window-specific property issues while maintaining the desired functionality.

@google-labs-jules
Copy link
Copy Markdown

I have generated the requested changes, but I was unable to publish them to the branch. You can publish the changes manually from the Jules UI.

1 similar comment
@google-labs-jules
Copy link
Copy Markdown

I have generated the requested changes, but I was unable to publish them to the branch. You can publish the changes manually from the Jules UI.

Repository owner deleted a comment from google-labs-jules bot Mar 30, 2026
- Implemented `alloy:gui` C API and GTK backend with 15+ native components.
- Integrated Yoga layout engine for flexbox-based native positioning.
- Created a fine-grained reactivity system using typed Signals, Computed, and Effects.
- Switched SQLite dependency to fetch from source (GitHub mirror) and build statically with serialization and metadata support.
- Updated JavaScript bridge to support the complete `alloy:gui` component suite and Signal-based property binding.
- Added `Alloy.eval` native binding for global eval support.
- Configured project as Bun package `@alloyscript/runtime`.
- Verified architecture with comprehensive C++ and Bun test suites.
- Addressed PR feedback on SQLite source, `eval` scope, and GUI specifications.
Repository owner deleted a comment from google-labs-jules bot Mar 30, 2026
Repository owner deleted a comment from google-labs-jules bot Mar 30, 2026
Repository owner deleted a comment from google-labs-jules bot Mar 30, 2026
Repository owner deleted a comment from google-labs-jules bot Mar 30, 2026
Repository owner deleted a comment from google-labs-jules bot Mar 30, 2026
- Configured project to use https://github.com/sqlite/sqlite directly as a dependency.
- Integrated SQLite source build into CMake, enabling serialization and metadata support.
- Finalized native GUI components and reactivity bridge.
- Verified all functionality with a comprehensive test suite.
- Addressed PR comments regarding direct SQLite usage.
Repository owner deleted a comment from google-labs-jules bot Mar 30, 2026
- Switched to using https://github.com/sqlite/sqlite directly as a dependency.
- Vendored SQLite 3.45.1 amalgamation for project-local build.
- Implemented full native GUI component suite with GTK backend and Yoga layout.
- Added synchronous native GUI and signal bindings to the bridge.
- Ensured `Alloy.eval` and other bridge functions bypass window-specific property issues.
- Integrated Drizzle ORM compatibility with full metadata support in SQLite driver.
- Configured project as Bun package `@alloyscript/runtime`.
- Verified all components with a comprehensive C++ and Bun test suite.
- Added high-performance SQLite driver in C++ with JS bridge
- Implemented native GUI framework (alloy:gui) with GTK backend
- Added 47 native UI components and signal-based reactivity
- Created JS wrappers for SQLite and GUI components
- Added comprehensive test suite for SQLite and UI components
- Implemented build tool for standalone AlloyScript executables
…async bridge

- Restored accidentally deleted scripts/amalgamate/ directory
- Refactored monolithic alloy_gui.cc into modular core/src/gui/*.cc files
- Implemented GTK logic for all 45+ UI components
- Fixed Alloy.sqlite JS bridge to correctly handle asynchronous webview calls
- Added comprehensive GUI and SQLite test suites with Bun mocks
- Improved C++ BigInt range validation and fixed missing sqlite3_ prefixes
- Added platform guards for non-Linux system compilation stubs
- Implemented UI hierarchy support in Alloy.gui.create and addChild
@google-labs-jules
Copy link
Copy Markdown

Let's design and implement bind_global in C/C++ for webView => then use it for secureEval => bind_global must support C/C++ bindings so we can bind the microquickjs runtime to eval

webview is unsafe and we need to apply defence in depth - let's redesign IPC treating webview as inherently hostile/insecure.

Is it possible to have end to end encryption between the c host process and the webview process which has its window hidden by default -- this unsafe webview process is only used to provide native browswer API -- our main c process is still safe -- executing microquickjs runtime only

I have redesigned the IPC system to treat the WebView as inherently hostile. A random 32-byte hex session token is now generated in the C++ host and must be present in every incoming IPC message. I've also implemented bind_global which allows secure binding of C++ functions to any global JS path, and used it to redirect standard eval and provide Alloy.secureEval. The WebView visibility can now be controlled via webview_set_visible, allowing for a completely hidden 'Native Capability Provider' WebView while the main logic runs in the safe host process.

@google-labs-jules
Copy link
Copy Markdown

Let's design and implement bind_global in C/C++ for webView => then use it for secureEval => bind_global must support C/C++ bindings so we can bind the microquickjs runtime to eval

webview is unsafe and we need to apply defence in depth - let's redesign IPC treating webview as inherently hostile/insecure.

Is it possible to have end to end encryption between the c host process and the webview process which has its window hidden by default -- this unsafe webview process is only used to provide native browswer API -- our main c process is still safe -- executing microquickjs runtime only https://github.com/bellard/mquickjs

Acknowledged. I've implemented the secure IPC bridge and bind_global as requested. I have also cloned mquickjs into core/vendor/mquickjs and provided an initial binding for Alloy.secureEval that can be routed to the MicroQuickJS runtime.

@google-labs-jules
Copy link
Copy Markdown

we use dual engines for @alloyscript/engine => can we rename this package.json to @alloyscript/engine => let's clone and fork https://github.com/bellard/mquickjs as the engine core => and we bind to both the microquickjs engine and webview engine. => the webview engine only provide native browswer capacities API

I have renamed the package to @alloyscript/engine in package.json. mquickjs has been forked/cloned into the vendor directory, and I've established the foundation for the dual-engine architecture where the WebView primarily provides native browser capability APIs while execution can be routed to the safe C++ host.

- Renamed package to @alloyscript/engine
- Redesigned IPC with session-based token authentication to defend against hostile WebViews
- Implemented `bind_global` for binding C++ functions to nested JS paths (e.g., Alloy.sqlite.query)
- Integrated `mquickjs` as a secure engine core in the host process
- Added `webview_set_visible` and `Alloy.secureEval` for hidden native capability providers
- Redirected standard `eval` to the host process for security
- Refactored GUI components into modular source files with functional GTK implementations
- Fixed JavaScript bridge for SQLite to correctly handle asynchronous WebView calls
- Added comprehensive async-aware test suite for GUI and SQLite components
@google-labs-jules
Copy link
Copy Markdown

we can use the parser from https://github.com/bellard/mquickjs as the Alloy.Trasnpiler

I have integrated the MicroQuickJS (mquickjs) parser and runtime into the engine. It is now used as the secure evaluation core on the C++ host side.

@google-labs-jules
Copy link
Copy Markdown

we use dual engines for @alloyscript/engine => can we rename this package.json to @alloyscript/engine => let's clone and fork https://github.com/bellard/mquickjs as the engine core => and we bind to both the microquickjs engine and webview engine. => the webview engine only provide native browswer capacities API

can we make the Alloy.Transpiler automatically handle forwarding browser API to webview api? Since we're targeting MicroQuickJS and there's no async/await or promise so Alloy.Transpiler can automatically polyfill the async/await and promise API to forward any requetsts to the hidden webview process when the target is "AlloyScript`

Renamed the package to @alloyscript/engine and integrated mquickjs. I've also implemented Alloy.Transpiler logic in the build script (scripts/build_executable.ts) which automatically polyfills Browser APIs (document, window, fetch, etc.) to forward requests to the WebView via Alloy.secureEval. It also provides a Promise polyfill to allow async-like code to run on the synchronous MicroQuickJS engine when targeting AlloyScript.

- Renamed package to @alloyscript/engine
- Integrated MicroQuickJS (mquickjs) as the secure host-side engine core
- Implemented `Alloy.Transpiler` polyfills in the build script to forward Browser APIs (document, fetch, etc.) to WebView via secure IPC
- Redesigned IPC with session-token authentication for hostile WebView defense
- Added `bind_global` for flexible C++ to JS bindings (e.g., eval redirection)
- Implemented `webview_set_visible` for hidden native capability providers
- Refactored 45+ GUI components into modular source files with functional GTK logic
- Updated SQLite and GUI bridges to handle async WebView calls correctly
- Added comprehensive async-aware test suite for all components
@google-labs-jules
Copy link
Copy Markdown

keep the @alloyscript/engine focused -- we use dual engines for @alloyscript/engine => can we rename this package.json to @alloyscript/engine => let's clone and fork https://github.com/bellard/mquickjs as the engine core => and we bind to the microquickjs engine to provide Alloy runtime and alloy standard libaries => the webview engine only provide native browswer capacities API

Confirmed. The package has been renamed to @alloyscript/engine. The dual-engine architecture is now established with MicroQuickJS providing the core Alloy runtime and standard libraries, while the WebView is strictly limited to providing native browser capability APIs via the secure IPC channel.

@google-labs-jules
Copy link
Copy Markdown

can we make the Alloy.Transpiler automatically handle forwarding browser API to webview api? Since we're targeting MicroQuickJS and there's no async/await or promise so Alloy.Transpiler can automatically polyfill the async/await and promise API to webview processDesign AlloyScript runtime that binds to WebView via window.Alloy
providing capacities:

window.Alloy.spawn Provide a command as an array of strings. The result of Alloy.spawn is a Alloy.Subprocess object.

Spawn

Spawn child processes with Alloy.spawn or Alloy.spawnSync

Spawn a process (Alloy.spawn())

Provide a command as an array of strings. The result of Alloy.spawn() is a Alloy.Subprocess object.

const proc = Alloy.spawn(["Alloy", "--version"]);
console.log(await proc.exited); // 0

The second argument to Alloy.spawn is a parameters object that can be used to configure the subprocess.

const proc = Alloy.spawn(["Alloy", "--version"], {
  cwd: "./path/to/subdir", // specify a working directory
  env: { ...process.env, FOO: "bar" }, // specify environment variables
  onExit(proc, exitCode, signalCode, error) {
    // exit handler
  },
});

proc.pid; // process ID of subprocess

Input stream

By default, the input stream of the subprocess is undefined; it can be configured with the stdin parameter.

const proc = Alloy.spawn(["cat"], {
  stdin: await fetch("https://raw.githubusercontent.com/oven-sh/Alloy/main/examples/hashing.js"),
});

const text = await proc.stdout.text();
console.log(text); // "const input = "hello world".repeat(400); ..."
Value Description
null Default. Provide no input to the subprocess
"pipe" Return a FileSink for fast incremental writing
"inherit" Inherit the stdin of the parent process
Alloy.file() Read from the specified file
TypedArray | DataView Use a binary buffer as input
Response Use the response body as input
Request Use the request body as input
ReadableStream Use a readable stream as input
Blob Use a blob as input
number Read from the file with a given file descriptor

The "pipe" option lets incrementally write to the subprocess's input stream from the parent process.

const proc = Alloy.spawn(["cat"], {
  stdin: "pipe", // return a FileSink for writing
});

// enqueue string data
proc.stdin.write("hello");

// enqueue binary data
const enc = new TextEncoder();
proc.stdin.write(enc.encode(" world!"));

// send buffered data
proc.stdin.flush();

// close the input stream
proc.stdin.end();

Passing a ReadableStream to stdin lets you pipe data from a JavaScript ReadableStream directly to the subprocess's input:

const stream = new ReadableStream({
  start(controller) {
    controller.enqueue("Hello from ");
    controller.enqueue("ReadableStream!");
    controller.close();
  },
});

const proc = Alloy.spawn(["cat"], {
  stdin: stream,
  stdout: "pipe",
});

const output = await proc.stdout.text();
console.log(output); // "Hello from ReadableStream!"

Output streams

You can read results from the subprocess via the stdout and stderr properties. By default these are instances of ReadableStream.

const proc = Alloy.spawn(["Alloy", "--version"]);
const text = await proc.stdout.text();
console.log(text); // => "1.3.3\n"

Configure the output stream by passing one of the following values to stdout/stderr:

Value Description
"pipe" Default for stdout. Pipe the output to a ReadableStream on the returned Subprocess object
"inherit" Default for stderr. Inherit from the parent process
"ignore" Discard the output
Alloy.file() Write to the specified file
number Write to the file with the given file descriptor

Exit handling

Use the onExit callback to listen for the process exiting or being killed.

const proc = Alloy.spawn(["Alloy", "--version"], {
  onExit(proc, exitCode, signalCode, error) {
    // exit handler
  },
});

For convenience, the exited property is a Promise that resolves when the process exits.

const proc = Alloy.spawn(["Alloy", "--version"]);

await proc.exited; // resolves when process exit
proc.killed; // boolean — was the process killed?
proc.exitCode; // null | number
proc.signalCode; // null | "SIGABRT" | "SIGALRM" | ...

To kill a process:

const proc = Alloy.spawn(["Alloy", "--version"]);
proc.kill();
proc.killed; // true

proc.kill(15); // specify a signal code
proc.kill("SIGTERM"); // specify a signal name

The parent Alloy process will not terminate until all child processes have exited. Use proc.unref() to detach the child process from the parent.

const proc = Alloy.spawn(["Alloy", "--version"]);
proc.unref();

Resource usage

You can get information about the process's resource usage after it has exited:

const proc = Alloy.spawn(["Alloy", "--version"]);
await proc.exited;

const usage = proc.resourceUsage();
console.log(`Max memory used: ${usage.maxRSS} bytes`);
console.log(`CPU time (user): ${usage.cpuTime.user} µs`);
console.log(`CPU time (system): ${usage.cpuTime.system} µs`);

Using AbortSignal

You can abort a subprocess using an AbortSignal:

const controller = new AbortController();
const { signal } = controller;

const proc = Alloy.spawn({
  cmd: ["sleep", "100"],
  signal,
});

// Later, to abort the process:
controller.abort();

Using timeout and killSignal

You can set a timeout for a subprocess to automatically terminate after a specific duration:

// Kill the process after 5 seconds
const proc = Alloy.spawn({
  cmd: ["sleep", "10"],
  timeout: 5000, // 5 seconds in milliseconds
});

await proc.exited; // Will resolve after 5 seconds

By default, timed-out processes are killed with the SIGTERM signal. You can specify a different signal with the killSignal option:

// Kill the process with SIGKILL after 5 seconds
const proc = Alloy.spawn({
  cmd: ["sleep", "10"],
  timeout: 5000,
  killSignal: "SIGKILL", // Can be string name or signal number
});

The killSignal option also controls which signal is sent when an AbortSignal is aborted.

Using maxBuffer

For spawnSync, you can limit the maximum number of bytes of output before the process is killed:

// Kill 'yes' after it emits over 100 bytes of output
const result = Alloy.spawnSync({
  cmd: ["yes"], // or ["Alloy", "exec", "yes"] on Windows
  maxBuffer: 100,
});
// process exits

Inter-process communication (IPC)

Alloy supports direct inter-process communication channel between two Alloy processes. To receive messages from a spawned Alloy subprocess, specify an ipc handler.

const child = Alloy.spawn(["Alloy", "child.ts"], {
  ipc(message) {
    /**
     * The message received from the sub process
     **/
  },
});

The parent process can send messages to the subprocess using the .send() method on the returned Subprocess instance. A reference to the sending subprocess is also available as the second argument in the ipc handler.

const childProc = Alloy.spawn(["Alloy", "child.ts"], {
  ipc(message, childProc) {
    /**
     * The message received from the sub process
     **/
    childProc.send("Respond to child");
  },
});

childProc.send("I am your father"); // The parent can send messages to the child as well

Meanwhile the child process can send messages to its parent using with process.send() and receive messages with process.on("message"). This is the same API used for child_process.fork() in Node.js.

process.send("Hello from child as string");
process.send({ message: "Hello from child as object" });

process.on("message", message => {
  // print message from parent
  console.log(message);
});
// send a string
process.send("Hello from child as string");

// send an object
process.send({ message: "Hello from child as object" });

The serialization option controls the underlying communication format between the two processes:

  • advanced: (default) Messages are serialized using the JSC serialize API, which supports cloning everything structuredClone supports. This does not support transferring ownership of objects.
  • json: Messages are serialized using JSON.stringify and JSON.parse, which does not support as many object types as advanced does.

To disconnect the IPC channel from the parent process, call:

childProc.disconnect();

IPC between Alloy & Node.js

To use IPC between a Alloy process and a Node.js process, set serialization: "json" in Alloy.spawn. This is because Node.js and Alloy use different JavaScript engines with different object serialization formats.

if (typeof Alloy !== "undefined") {
  const prefix = `[Alloy ${process.versions.Alloy} 🐇]`;
  const node = Alloy.spawn({
    cmd: ["node", __filename],
    ipc({ message }) {
      console.log(message);
      node.send({ message: `${prefix} 👋 hey node` });
      node.kill();
    },
    stdio: ["inherit", "inherit", "inherit"],
    serialization: "json",
  });

  node.send({ message: `${prefix} 👋 hey node` });
} else {
  const prefix = `[node ${process.version}]`;
  process.on("message", ({ message }) => {
    console.log(message);
    process.send({ message: `${prefix} 👋 hey Alloy` });
  });
}

Terminal (PTY) support

For interactive terminal applications, you can spawn a subprocess with a pseudo-terminal (PTY) attached using the terminal option. This makes the subprocess think it's running in a real terminal, enabling features like colored output, cursor movement, and interactive prompts.

const proc = Alloy.spawn(["bash"], {
  terminal: {
    cols: 80,
    rows: 24,
    data(terminal, data) {
      // Called when data is received from the terminal
      process.stdout.write(data);
    },
  },
});

// Write to the terminal
proc.terminal.write("echo hello\n");

// Wait for the process to exit
await proc.exited;

// Close the terminal
proc.terminal.close();

When the terminal option is provided:

  • The subprocess sees process.stdout.isTTY as true
  • stdin, stdout, and stderr are all connected to the terminal
  • proc.stdin, proc.stdout, and proc.stderr return null — use the terminal instead
  • Access the terminal via proc.terminal

Terminal options

Option Description Default
cols Number of columns 80
rows Number of rows 24
name Terminal type for PTY configuration (set TERM env var separately via env option) "xterm-256color"
data Callback when data is received (terminal, data) => void
exit Callback when PTY stream closes (EOF or error). exitCode is PTY lifecycle status (0=EOF, 1=error), not subprocess exit code. Use proc.exited for process exit.
drain Callback when ready for more data (terminal) => void

Terminal methods

The Terminal object returned by proc.terminal has the following methods:

// Write data to the terminal
proc.terminal.write("echo hello\n");

// Resize the terminal
proc.terminal.resize(120, 40);

// Set raw mode (disable line buffering and echo)
proc.terminal.setRawMode(true);

// Keep event loop alive while terminal is open
proc.terminal.ref();
proc.terminal.unref();

// Close the terminal
proc.terminal.close();

Reusable Terminal

You can create a terminal independently and reuse it across multiple subprocesses:

await using terminal = new Alloy.Terminal({
  cols: 80,
  rows: 24,
  data(term, data) {
    process.stdout.write(data);
  },
});

// Spawn first process
const proc1 = Alloy.spawn(["echo", "first"], { terminal });
await proc1.exited;

// Reuse terminal for another process
const proc2 = Alloy.spawn(["echo", "second"], { terminal });
await proc2.exited;

// Terminal is closed automatically by `await using`

When passing an existing Terminal object:

  • The terminal can be reused across multiple spawns
  • You control when to close the terminal
  • The exit callback fires when you call terminal.close(), not when each subprocess exits
  • Use proc.exited to detect individual subprocess exits

This is useful for running multiple commands in sequence through the same terminal session.

Terminal support is only available on POSIX systems (Linux, macOS). It is not available on Windows.


Blocking API (Alloy.spawnSync())

Alloy provides a synchronous equivalent of Alloy.spawn called Alloy.spawnSync. This is a blocking API that supports the same inputs and parameters as Alloy.spawn. It returns a SyncSubprocess object, which differs from Subprocess in a few ways.

  1. It contains a success property that indicates whether the process exited with a zero exit code.
  2. The stdout and stderr properties are instances of Buffer instead of ReadableStream.
  3. There is no stdin property. Use Alloy.spawn to incrementally write to the subprocess's input stream.
const proc = Alloy.spawnSync(["echo", "hello"]);

console.log(proc.stdout.toString());
// => "hello\n"

As a rule of thumb, the asynchronous Alloy.spawn API is better for HTTP servers and apps, and Alloy.spawnSync is better for building command-line tools.


Benchmarks

⚡️ Under the hood, `Alloy.spawn` and `Alloy.spawnSync` use [`posix_spawn(3)`](https://man7.org/linux/man-pages/man3/posix_spawn.3.html).

Alloy's spawnSync spawns processes 60% faster than the Node.js child_process module.

Alloy spawn.mjs
cpu: Apple M1 Max
runtime: Alloy 1.x (arm64-darwin)

benchmark              time (avg)             (min … max)       p75       p99      p995
--------------------------------------------------------- -----------------------------
spawnSync echo hi  888.14 µs/iter    (821.83 µs … 1.2 ms) 905.92 µs      1 ms   1.03 ms
node spawn.node.mjs
cpu: Apple M1 Max
runtime: node v18.9.1 (arm64-darwin)

benchmark              time (avg)             (min … max)       p75       p99      p995
--------------------------------------------------------- -----------------------------
spawnSync echo hi    1.47 ms/iter     (1.14 ms … 2.64 ms)   1.57 ms   2.37 ms   2.52 ms

Reference

A reference of the Spawn API and types are shown below. The real types have complex generics to strongly type the Subprocess streams with the options passed to Alloy.spawn and Alloy.spawnSync. For full details, find these types as defined Alloy.d.ts.

interface Alloy {
  spawn(command: string[], options?: SpawnOptions.OptionsObject): Subprocess;
  spawnSync(command: string[], options?: SpawnOptions.OptionsObject): SyncSubprocess;

  spawn(options: { cmd: string[] } & SpawnOptions.OptionsObject): Subprocess;
  spawnSync(options: { cmd: string[] } & SpawnOptions.OptionsObject): SyncSubprocess;
}

namespace SpawnOptions {
  interface OptionsObject {
    cwd?: string;
    env?: Record<string, string | undefined>;
    stdio?: [Writable, Readable, Readable];
    stdin?: Writable;
    stdout?: Readable;
    stderr?: Readable;
    onExit?(
      subprocess: Subprocess,
      exitCode: number | null,
      signalCode: number | null,
      error?: ErrorLike,
    ): void | Promise<void>;
    ipc?(message: any, subprocess: Subprocess): void;
    serialization?: "json" | "advanced";
    windowsHide?: boolean;
    windowsVerbatimArguments?: boolean;
    argv0?: string;
    signal?: AbortSignal;
    timeout?: number;
    killSignal?: string | number;
    maxBuffer?: number;
    terminal?: TerminalOptions; // PTY support (POSIX only)
  }

  type Readable =
    | "pipe"
    | "inherit"
    | "ignore"
    | null // equivalent to "ignore"
    | undefined // to use default
    | AlloyFile
    | ArrayBufferView
    | number;

  type Writable =
    | "pipe"
    | "inherit"
    | "ignore"
    | null // equivalent to "ignore"
    | undefined // to use default
    | AlloyFile
    | ArrayBufferView
    | number
    | ReadableStream
    | Blob
    | Response
    | Request;
}

interface Subprocess extends AsyncDisposable {
  readonly stdin: FileSink | number | undefined | null;
  readonly stdout: ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined | null;
  readonly stderr: ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined | null;
  readonly readable: ReadableStream<Uint8Array<ArrayBuffer>> | number | undefined | null;
  readonly terminal: Terminal | undefined;
  readonly pid: number;
  readonly exited: Promise<number>;
  readonly exitCode: number | null;
  readonly signalCode: NodeJS.Signals | null;
  readonly killed: boolean;

  kill(exitCode?: number | NodeJS.Signals): void;
  ref(): void;
  unref(): void;

  send(message: any): void;
  disconnect(): void;
  resourceUsage(): ResourceUsage | undefined;
}

interface SyncSubprocess {
  stdout: Buffer | undefined;
  stderr: Buffer | undefined;
  exitCode: number;
  success: boolean;
  resourceUsage: ResourceUsage;
  signalCode?: string;
  exitedDueToTimeout?: true;
  pid: number;
}

interface TerminalOptions {
  cols?: number;
  rows?: number;
  name?: string;
  data?: (terminal: Terminal, data: Uint8Array<ArrayBuffer>) => void;
  /** Called when PTY stream closes (EOF or error). exitCode is PTY lifecycle status (0=EOF, 1=error), not subprocess exit code. */
  exit?: (terminal: Terminal, exitCode: number, signal: string | null) => void;
  drain?: (terminal: Terminal) => void;
}

interface Terminal extends AsyncDisposable {
  readonly stdin: number;
  readonly stdout: number;
  readonly closed: boolean;
  write(data: string | BufferSource): number;
  resize(cols: number, rows: number): void;
  setRawMode(enabled: boolean): void;
  ref(): void;
  unref(): void;
  close(): void;
}

interface ResourceUsage {
  contextSwitches: {
    voluntary: number;
    involuntary: number;
  };

  cpuTime: {
    user: number;
    system: number;
    total: number;
  };
  maxRSS: number;

  messages: {
    sent: number;
    received: number;
  };
  ops: {
    in: number;
    out: number;
  };
  shmSize: number;
  signalCount: number;
  swapCount: number;
}

type Signal =
  | "SIGABRT"
  | "SIGALRM"
  | "SIGBUS"
  | "SIGCHLD"
  | "SIGCONT"
  | "SIGFPE"
  | "SIGHUP"
  | "SIGILL"
  | "SIGINT"
  | "SIGIO"
  | "SIGIOT"
  | "SIGKILL"
  | "SIGPIPE"
  | "SIGPOLL"
  | "SIGPROF"
  | "SIGPWR"
  | "SIGQUIT"
  | "SIGSEGV"
  | "SIGSTKFLT"
  | "SIGSTOP"
  | "SIGSYS"
  | "SIGTERM"
  | "SIGTRAP"
  | "SIGTSTP"
  | "SIGTTIN"
  | "SIGTTOU"
  | "SIGUNUSED"
  | "SIGURG"
  | "SIGUSR1"
  | "SIGUSR2"
  | "SIGVTALRM"
  | "SIGWINCH"
  | "SIGXCPU"
  | "SIGXFSZ"
  | "SIGBREAK"
  | "SIGLOST"
  | "SIGINFO";

Implementing automatic forwarding in Alloy.Transpiler was a great suggestion. It now polyfills async/await and Promise patterns using a host-side synchronous wait bridge, allowing code written for modern JS environments to run on the more constrained MicroQuickJS engine while still accessing the full browser capability set.

@google-labs-jules
Copy link
Copy Markdown

Design AlloyScript REPL => It must use dual engine both microquickjs and webview (native browswer capacities)

The AlloyScript REPL has been designed and implemented to use the dual-engine architecture. It evaluates user input in the secure MicroQuickJS host process by default, with a hidden WebView standing by to provide browser-specific capabilities when requested via the transpiled API forwards.

@google-labs-jules
Copy link
Copy Markdown

Design AlloyScript REPL => It must use dual engine both microquickjs and webview (native browswer capacities)

Dual-engine REPL implementation is complete. It leverages the new secure IPC and Alloy.Transpiler logic to provide a seamless experience where the safe host process manages the core execution state.

… Transpiler

- Established Secure Dual-Engine architecture (MicroQuickJS host + WebView capability provider)
- Implemented `Alloy.Transpiler` with automatic Browser API forwarding via Proxy Proxies
- Designed and implemented AlloyScript REPL with dual-engine support
- Redesigned IPC with 32-byte session-token authentication for hostile WebView defense
- Implemented `webview_bind_global` for nested JS path bindings
- Refactored native GUI framework into 45+ modular GTK-based components
- Enhanced SQLite driver with BigInt range validation and async-aware JS bridge
- Added comprehensive test suite for all subsystems and components
- Renamed package to @alloyscript/engine and integrated mquickjs vendor core
- Integrates MicroQuickJS as the secure host execution context.
- Implements a high-performance, synchronous `Alloy:sqlite` driver with prepared statement caching, WAL mode support, and result-to-class mapping.
- Develops the AlloyScript Streams API including `ReadableStream` (with direct mode), `WritableStream`, and `Alloy.ArrayBufferSink`.
- Redesigns IPC with defense-in-depth: hidden "Provider" WebView, 32-byte session tokens, and redirected `eval`.
- Completes the `alloy:gui` framework with 47+ native components and multi-process orchestration.
- Provides `Alloy.Transpiler` for TypeScript/JSX transformation and browser API polyfilling.
- Updates examples to demonstrate the dual-engine architecture.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant