# How JSAR Implements Event Communication Between Processes

> Discover how JSAR implements event communication via a native-event bridge. Explore bidirectional RPC and document lifecycle messaging between JS runtime and native host processes.

- Repository: [M Creative Lab/jsar-runtime](https://github.com/m-creativelab/jsar-runtime)
- Tags: internals
- Published: 2026-03-06

---

**JSAR implements event communication between processes through a native-event bridge that maps numeric event types to JavaScript Event objects, enabling bidirectional RPC and document lifecycle messaging between the JavaScript runtime and native host processes.**

The JSAR (JavaScript Augmented Reality) runtime, maintained at `m-creativelab/jsar-runtime`, requires robust event communication between processes to coordinate between JavaScript execution contexts and native engines like Unity or Unreal Engine. This article examines the three-layer architecture that handles these cross-process events, from the native C++/Rust bindings to the high-level JavaScript APIs.

## The Native-Event Bridge Architecture

JSAR's event communication relies on a **native-event bridge** that wraps a C++/Rust channel exposed through the `transmute:messaging` binding. The architecture separates concerns across three distinct layers.

### Native Event Target Layer

The foundation resides in [`lib/bindings/messaging.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/lib/bindings/messaging.ts) (lines 1-11) and the TypeScript declarations in [`types/transmute-private.d.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/types/transmute-private.d.ts) (lines 185-207). This layer wraps the native `transmute:messaging` class and exposes `dispatchEvent` and `dispose` methods. It defines numeric `EventTypes` for categorizing messages as RPC requests, document requests, or document lifecycle events.

```typescript
const { NativeEventTarget } = process._linkedBinding('transmute:messaging');
const nativeEventTarget = new NativeEventTarget(onNativeEventListener);

```

### JavaScript Event Dispatcher Layer

A thin wrapper around the native target translates raw numeric events into high-level DOM-like Event objects. The `onNativeEventListener` callback (lines 70-102 in [`messaging.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/messaging.ts)) receives native events, switches on the `eventType` parameter, and dispatches parsed events onto a standard JavaScript `EventTarget`.

For RPC responses, the listener retrieves the pending callback from `RpcRequestWaitlist` (lines 77-81) and resolves the promise. For document requests, it constructs a `DocumentRequestEvent` and dispatches it (lines 92-94).

### Process-Specific Adapters

The system adapts to different execution contexts:

- **Main thread**: Exposes `addEventListener`, `addDocumentRequestListener`, `makeRpcCall`, and `reportDocumentEvent` (lines 13-30 in [`messaging.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/messaging.ts)).
- **WebWorker**: The entry script [`jsar-webworkers-entry.js`](https://github.com/m-creativelab/jsar-runtime/blob/main/jsar-webworkers-entry.js) (bootstrapped in [`lib/webworkers/entry.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/lib/webworkers/entry.ts), lines 31-55) creates a `WorkerContext` that forwards `postMessage`-style events while maintaining access to the same native messaging channel.

## Event Communication APIs in the Main Thread

The main thread API abstracts the native bridge into Promise-based methods and standard event listeners.

### RPC Calls with makeRpcCall

The `makeRpcCall(method, args)` function (implemented in [`lib/bindings/messaging.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/lib/bindings/messaging.ts), lines 91-130) initiates cross-process RPC. It generates a unique request ID via `dispatchEventToHost`, stores the resolver in `RpcRequestWaitlist`, and returns a Promise that settles when the native host sends back a response event.

```typescript
import { makeRpcCall } from './lib/bindings/messaging';

makeRpcCall('getSessionInfo', [])
  .then(info => console.log('Session info:', info))
  .catch(err => console.error('RPC error:', err));

```

### Handling Document Requests

`addDocumentRequestListener(cb)` (lines 26-28) registers listeners for `DocumentRequestEvent.Name`, allowing JavaScript to respond when the host process requests document loads. The native side fires events that the dispatcher transforms into `DocumentRequestEvent` instances (lines 92-94).

### Reporting Document Lifecycle Events

`reportDocumentEvent(documentId, eventName)` (lines 71-78) notifies the host of document lifecycle changes (load, error, etc.). It maps string event names to numeric `DocumentEventTypes` defined in the native class and dispatches through `nativeEventTarget`.

## WebWorker Event Communication

WebWorkers in JSAR share the same native bridge, enabling consistent event communication between processes across threads.

### Worker Entry Point Configuration

The worker bootstrap in [`lib/webworkers/entry.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/lib/webworkers/entry.ts) (lines 31-55) executes inside the Node.js `WorkerThread`. It sets up global `postMessage` forwarding to the parent port (lines 10-14), injects the `__WorkerImpl` constructor for nested worker creation (lines 21-27), and instantiates a `WorkerContext` bound to the native `transmute:dom` binding (lines 32-34).

```typescript
Object.defineProperty(globalThis, 'postMessage', { 
  value: parentPort.postMessage.bind(parentPort) 
});
const workerContext = new WorkerContext(workerRequest.baseURI, workerRequest?.options);

```

### Shared Native Bridge Access

Inside workers, `makeRpcCall` and document event APIs function identically to the main thread because `WorkerContext` maintains a connection to the same `NativeEventTarget`. The `parentPort` forwards messages from the host thread to the `WorkerContext`, which then dispatches events through the standard bridge.

## Implementation Examples

### Sending RPC Requests from JavaScript

This pattern initiates host communication and handles the asynchronous response through the native bridge:

```typescript
import { makeRpcCall } from './lib/bindings/messaging';

// Call a native method named "loadDocument"
makeRpcCall('loadDocument', [{ url: 'https://example.com/model.glb', documentId: 42 }])
  .then(data => console.log('Document loaded', data))
  .catch(err => console.error('Load failed:', err));

```

The implementation stores the resolver in `RpcRequestWaitlist` (lines 91-130) and invokes it when `onNativeEventListener` receives the matching `RpcResponse` event (lines 77-81).

### Handling Document Requests in Workers

Workers can listen for host-initiated document requests and report completion:

```typescript
import { addDocumentRequestListener, reportDocumentEvent } from './lib/bindings/messaging';

addDocumentRequestListener(async (event) => {
  const resp = await fetch(event.url);
  const blob = await resp.blob();
  // Process the blob...
  
  // Notify host the document has loaded
  reportDocumentEvent(event.documentId, 'load');
});

```

`addDocumentRequestListener` hooks into `DocumentRequestEvent.Name` (lines 26-28), while `reportDocumentEvent` dispatches to the native target (lines 71-78).

### Spawning Workers with Event Support

Create workers that maintain full event bridge access:

```typescript
import { WorkerImpl } from './lib/webworkers/worker';

const worker = new WorkerImpl('worker-script.js');
worker.onmessage = ev => console.log('Worker says:', ev.data);
worker.start();

```

`WorkerImpl.start()` resolves the entry script path (lines 52-55 in [`worker.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/worker.ts)) and spawns the thread (lines 52-64), ensuring the worker initializes with the [`jsar-webworkers-entry.js`](https://github.com/m-creativelab/jsar-runtime/blob/main/jsar-webworkers-entry.js) bridge setup.

## Summary

- **JSAR** implements event communication between processes through a **native-event bridge** wrapping the `transmute:messaging` binding.
- The architecture separates concerns into **Native Event Target**, **JavaScript Event Dispatcher**, and **Process-Specific Adapter** layers.
- **RPC calls** use a waitlist pattern where `makeRpcCall` generates request IDs and `onNativeEventListener` resolves pending promises via `RpcRequestWaitlist`.
- **Document lifecycle events** travel through standardized `EventTarget` interfaces, with `addDocumentRequestListener` and `reportDocumentEvent` providing high-level abstractions.
- **WebWorkers** share the same native bridge through `WorkerContext` initialization in [`lib/webworkers/entry.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/lib/webworkers/entry.ts), enabling consistent cross-process messaging across execution contexts.

## Frequently Asked Questions

### How does JSAR route RPC responses back to the correct JavaScript caller?

When `makeRpcCall` dispatches an RPC request to the host, it stores the promise resolver in a `RpcRequestWaitlist` Map keyed by request ID (lines 91-130 in [`messaging.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/messaging.ts)). When the native host responds, `onNativeEventListener` extracts the request ID from the event, retrieves the corresponding callback from the waitlist (lines 77-81), and invokes it with the response payload, thereby resolving the original Promise.

### Can WebWorkers access the same native event bridge as the main thread?

Yes. The worker entry script in [`lib/webworkers/entry.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/lib/webworkers/entry.ts) (lines 32-34) creates a `WorkerContext` bound to the `transmute:dom` native binding, which shares the same underlying `transmute:messaging` channel. This allows workers to call `makeRpcCall`, `reportDocumentEvent`, and other bridge APIs exactly as the main thread does.

### What event types does the NativeEventTarget support?

According to [`types/transmute-private.d.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/types/transmute-private.d.ts) (lines 185-207) and the implementation in [`lib/bindings/messaging.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/lib/bindings/messaging.ts) (lines 7-12), the native bridge supports numeric event types including `RpcRequest`, `RpcResponse`, `DocumentRequest`, and various `DocumentEventTypes` for lifecycle signaling. These constants map to symbolic names in the JavaScript `EventType` enum.

### How does JSAR report document lifecycle events to the host process?

JavaScript code calls `reportDocumentEvent(documentId, eventName)`, which looks up the numeric `DocumentEventTypes` code for the provided string name (e.g., 'load', 'error'), then dispatches a structured event through `nativeEventTarget.dispatchEvent` (lines 71-78 in [`messaging.ts`](https://github.com/m-creativelab/jsar-runtime/blob/main/messaging.ts)). The native host receives this as a `DocumentEvent` type and processes it accordingly.