Skip to content

JavaScript API reference

This page documents the raw JavaScript surface exposed by the EngawaUI Unreal plugin and the named-event facade supplied by @studiouwu/engawa.

For step-by-step setup, see Send data between JavaScript and Unreal and Use the EngawaUI Event component.

Availability

The bridge is installed on the main page before its authored scripts execute:

globalThis.engawa

Because a browser Window is the global object, window.engawa refers to the same value.

const isRunningInEngawaUI = Boolean(
  globalThis.engawa && typeof globalThis.engawa.postMessage === "function"
);

The bridge does not exist when the HTML file is opened in Chrome, Edge, Firefox, Safari, or another ordinary browser.

globalThis.engawa

engawa is a frozen object installed as a read-only, non-configurable global. Application code cannot replace it or add methods to it.

Conceptual type declaration:

interface EngawaUIBridge {
  postMessage(value: JSONValue): void;
}

type JSONPrimitive = string | number | boolean | null;
type JSONValue =
  | JSONPrimitive
  | JSONValue[]
  | { [key: string]: JSONValue };

declare const engawa: Readonly<EngawaUIBridge>;

engawa.postMessage(value)

Sends one JSON-compatible JavaScript value from the page to the owning Engawa UI Component.

engawa.postMessage({
  type: "menu-action",
  action: "start-game",
  sequence: 7
});
Detail Behavior
Parameters Exactly one JSON-compatible value
Return value JavaScript undefined after synchronous native admission
Unreal receiver On Page Message(Json Value)
Unreal value type Serialized JSON text
Delivery Asynchronous, bounded, at-most-once

The bridge applies JSON.stringify internally. Pass objects and arrays directly rather than stringifying them first.

Serialization happens before the native call, so postMessage throws synchronously when JSON.stringify throws—for example, for a cycle or BigInt. It also throws synchronously if the native bridge rejects admission. Only JSON-compatible top-level values are supported; values such as undefined or a function do not produce a JSON string and are not valid messages. On success, the function returns JavaScript undefined; there is no result or acknowledgement channel in that return value.

Normal JavaScript JSON behavior still applies. For example, a finite number remains a number, unsupported object properties may be omitted, and NaN or infinity serialize as null. Prefer deliberately constructed data-only objects instead of relying on those conversions.

try {
  engawa.postMessage({ type: "ready", value: 42 });
} catch (error) {
  console.error("The message could not be serialized or admitted", error);
}

Do not treat a non-throwing call as an acknowledgement from Blueprint. If application confirmation matters, send a separate response message carrying your own request ID.

engawa-message

Unreal-to-page messages dispatch a CustomEvent on window/globalThis:

function onEngawaMessage(event) {
  console.log(event.detail);
}

window.addEventListener("engawa-message", onEngawaMessage);

// Later, if this code no longer owns the listener:
window.removeEventListener("engawa-message", onEngawaMessage);

event.detail is the parsed value represented by the JSON string passed to Blueprint's Post Message to Page node.

Blueprint JSON input JavaScript event.detail type
{"health":100} Object
[1,2,3] Array
"hello" String
42 Number
false Boolean
null null

The DOM event does not carry a return channel. Send a separate page message if the page needs to acknowledge the host message.

@studiouwu/engawa and globalThis.EngawaUIEvents

The native runtime installs globalThis.engawa, but it does not install the named-event facade. The recommended setup is to add the published package to a Vite or other bundled web project:

npm add @studiouwu/engawa

Import its default export in the application entry module:

import EngawaUIEvents from "@studiouwu/engawa";

That import creates the facade and also exposes the same object as globalThis.EngawaUIEvents. The package includes TypeScript declarations for the facade, window.engawa, and the engawa-message DOM event.

The package import must be resolved by a bundler; EngawaUI does not resolve a bare package specifier in page JavaScript. Build the application and import the bundler's output instead of trying to expose a node_modules tree to the page.

Type application events with the package

The package supports declaration merging for application-specific payloads. For example, create src/engawa-events.d.ts:

import "@studiouwu/engawa";

declare global {
  namespace EngawaUI {
    interface Events {
      "inventory.updated": {
        itemId: string;
        count: number;
      };
    }

    interface GameplayTagEvents {
      "UI.Inventory.Open": {
        slot: number;
      };
    }
  }
}

export {};

Calls using those names now receive their declared payload types through the imported facade and globalThis.EngawaUIEvents.

Use the global build without installing through npm

Download the pinned standalone browser build, save it inside the local web source, and load it before application code:

<script src="vendor/engawa.global.js"></script>
<script src="app.js" defer></script>

Do not use the CDN URL directly in an EngawaUI page; remote resources are blocked. The local standalone script installs the facade, which plain JavaScript can read explicitly:

const EngawaUIEvents = globalThis.EngawaUIEvents;

TypeScript users who choose this vendored-global path must provide a custom ambient declaration. The event setup guide contains a complete engawa-events.d.ts. That file supplies compile-time types only; it cannot install the runtime facade.

Conceptual declaration:

type EngawaUIEventListener = (
  payload: JSONValue,
  eventName: string
) => void;

type EngawaUIUnsubscribe = () => boolean;

interface EngawaUIReceivedEvent {
  readonly eventName: string;
  readonly payload: JSONValue;
}

interface EngawaUIWaitOptions {
  readonly signal?: AbortSignal;
  readonly timeoutMs?: number;
}

interface EngawaUIEventChannel {
  emit(eventName: string, payload?: JSONValue): true;
  addListener(
    eventName: string,
    listener: EngawaUIEventListener
  ): EngawaUIUnsubscribe;
  once(
    eventName: string,
    listener: EngawaUIEventListener
  ): EngawaUIUnsubscribe;
  removeListener(
    eventName: string,
    listener: EngawaUIEventListener
  ): boolean;
  clearListeners(eventName?: string): void;
  waitFor(
    eventName: string,
    options?: EngawaUIWaitOptions
  ): Promise<EngawaUIReceivedEvent>;
}

interface EngawaUIEventsFacade extends EngawaUIEventChannel {
  readonly protocolVersion: 1;
  readonly gameplayTags: Readonly<EngawaUIEventChannel>;
}

declare var EngawaUIEvents: Readonly<EngawaUIEventsFacade>;

EngawaUIEvents.protocolVersion

The facade's numeric event-envelope version. Its current value is 1.

console.log(EngawaUIEvents.protocolVersion); // 1

Do not use this value to invent a different envelope. Keep the npm package or standalone browser build compatible with the EngawaUI plugin version.

EngawaUIEvents.emit(eventName, payload?)

Sends a simple named event from JavaScript to an EngawaUI Event component.

EngawaUIEvents.emit("inventory.open", { slot: 3 });
EngawaUIEvents.emit("menu.closed"); // payload becomes null
Detail Behavior
Name matching Case-sensitive
Missing payload JSON null
Success return true after validation and a synchronous call to the raw bridge
Failure Throws for an invalid name, missing bridge, serialization failure, or synchronous native admission failure
Unreal receiver On Event Received(Event Name, Json Payload)

Its true return is not proof that a named-event component or Blueprint handler processed the event, and it is not an application acknowledgement.

EngawaUIEvents.addListener(eventName, listener)

Listens for a simple event emitted by an EngawaUI Event component.

function onInventoryUpdated(payload, eventName) {
  console.log(eventName, payload);
}

const unsubscribe = EngawaUIEvents.addListener(
  "inventory.updated",
  onInventoryUpdated
);

The callback receives the parsed payload first and the event name second. The method returns an idempotent unsubscribe function:

unsubscribe(); // true if it removed the subscription
unsubscribe(); // false; it was already removed

EngawaUIEvents.once(eventName, listener)

Registers a listener that removes itself before its first callback runs.

EngawaUIEvents.once("dialog.initialized", (payload, eventName) => {
  console.log(eventName, payload);
});

It returns the same kind of unsubscribe function as addListener, so the one-shot listener can also be cancelled before it fires.

EngawaUIEvents.waitFor(eventName, options?)

Returns a promise for the next matching event and removes its internal listener after the promise settles:

const response = await EngawaUIEvents.waitFor("inventory.response", {
  timeoutMs: 5000
});

console.log(response.eventName, response.payload);

timeoutMs must be finite and non-negative. The promise rejects when that timeout expires. Pass an AbortSignal as signal when application lifecycle code needs to cancel the wait. If the signal is already aborted, the returned promise rejects immediately with the signal's reason.

Call waitFor before emitting a request that could produce the response. Like the other listener APIs, waitFor does not turn EngawaUI events into RPCs; the application still defines the matching response event and request ID.

EngawaUIEvents.removeListener(eventName, listener)

Removes the exact function previously registered for the exact event name.

function onUpdate(payload) {
  console.log(payload);
}

EngawaUIEvents.addListener("inventory.updated", onUpdate);
const removed = EngawaUIEvents.removeListener("inventory.updated", onUpdate);

Returns true when the listener existed and was removed; otherwise returns false. An anonymous function cannot be removed later unless its reference was saved.

EngawaUIEvents.clearListeners(eventName?)

With a name, removes every simple listener for that exact event:

EngawaUIEvents.clearListeners("inventory.updated");

With no argument, removes every simple-channel listener:

EngawaUIEvents.clearListeners();

It does not clear Gameplay Tag listeners. Use the method on EngawaUIEvents.gameplayTags for that channel.

EngawaUIEvents.gameplayTags

The gameplayTags object has the same six methods as the simple channel, including waitFor. It uses the gameplay-tag envelope channel and is paired with the EngawaUI Gameplay Tag Event component.

EngawaUIEvents.gameplayTags.emit(
  "UI.Inventory.Item.Selected",
  { itemId: "potion-small" }
);

const unsubscribe = EngawaUIEvents.gameplayTags.addListener(
  "UI.Inventory.Item.Confirmed",
  (payload, eventName) => {
    console.log(eventName, payload);
  }
);

Gameplay Tag listener lookup is case-insensitive. Unreal accepts page tags only when an exact registered tag is selected in the component's Gameplay Tags To Listen For container. An empty container accepts none.

See Use Gameplay Tag events for the Unreal setup.

Event-name validation

Both facade channels require an event name that is:

  • a string;
  • non-empty;
  • no more than 256 UTF-16 code units (eventName.length); and
  • free of control characters from U+0000 through U+001F.

An invalid name throws TypeError in JavaScript. Simple names are case-sensitive; Gameplay Tag listener names are case-insensitive.

Listener scheduling and errors

The facade takes a stable snapshot of matching listeners, then invokes them in a JavaScript microtask. As a result:

  • a native engawa-message dispatch does not directly re-enter application callbacks;
  • adding or removing a listener during a callback does not alter the current snapshot;
  • one listener throwing does not prevent the remaining listeners from running; and
  • a thrown listener error is reported asynchronously with setTimeout.

The facade records its page-owned router on the Window. Reinstalling it—by loading the standalone build or importing a fresh bundled entry—removes the previous router and clears its listeners before installing the replacement.

Protocol envelope

The facade serializes this envelope:

interface EngawaUIEventEnvelope {
  protocol: "engawaui.event";
  version: 1;
  channel: "event" | "gameplay-tag";
  event: string;
  payload: JSONValue;
}

Example:

{
  "protocol": "engawaui.event",
  "version": 1,
  "channel": "event",
  "event": "inventory.open",
  "payload": {
    "slot": 3
  }
}

The Unreal component returns only event and a reserialized compact payload to Blueprint. A missing envelope payload is normalized to JSON null.

Use the facade rather than manually building envelopes. Wrong versions, missing fields, malformed JSON, and the wrong channel are invalid or ignored by the corresponding component.

Unreal-side API map

Direction/use Blueprint node or event Meaning
Page → Unreal, raw On Page Message Complete serialized JSON value
Unreal → page, raw Post Message to Page Accept a strict JSON value string for async delivery
Page → Unreal, named On Event Received Simple event name + compact JSON payload
Unreal → page, named Emit Event Prepare a named event asynchronously
Event failure On Event Error Validation, readiness, size, source, or queue error
Page → Unreal, tag On Gameplay Tag Event Received Typed registered tag + compact JSON payload
Unreal → page, tag Emit Gameplay Tag Event Emit a typed registered tag
One-off script Evaluate JavaScript Async Return request ID immediately
Script result On JavaScript Result Request ID, success, JSON result, exception

All of these Blueprint delegates run on Unreal's game thread.

Evaluate JavaScript Async

JavaScript evaluation is a Blueprint/C++ component operation rather than a page global, but its result follows the same JSON contract.

Evaluate JavaScript Async(Script) → Request Id
On JavaScript Result(Request Id, Success, Json Result, Exception)

Rules:

  • call it after On Page Loaded;
  • request ID 0 means the call was rejected;
  • correlate every result with its request ID;
  • successful values are returned as serialized JSON;
  • undefined becomes null;
  • thrown errors and non-JSON-compatible return values set Success = false;
  • plain values and promises that settle in the bounded evaluation turn are supported; arbitrary asynchronous promises are not waited on; and
  • EngawaUI has no synchronous JavaScript execution API.

Example script:

({ title: document.title, focused: document.hasFocus() })

Possible successful Json Result:

{"title":"Inventory","focused":true}

Limits and delivery guarantees

For EngawaUI Event and EngawaUI Gameplay Tag Event:

  • Max Event Bytes defaults to 65,536 bytes and is clamped to 1,024–65,536;
  • the limit covers the complete UTF-8 envelope;
  • each component keeps at most 64 active/queued preparation items per direction;
  • one item per direction is prepared at a time, preserving that component's order;
  • runtime transport queues impose additional limits;
  • a Blueprint emit's true output means accepted for component preparation, while a JavaScript facade emit's true only means it called the raw bridge without a synchronous exception;
  • delivery is at-most-once, with no automatic retry or acknowledgement; and
  • separate components and raw/navigation/evaluation APIs have independent queues, so cross-API order is not guaranteed.

For reliable application confirmation, include a request ID in the JSON payload and send a separate response event.

Quick examples

Page action to Blueprint

EngawaUIEvents.emit("menu.option.selected", {
  option: "graphics",
  index: 1
});

Blueprint receives:

Event Name   = menu.option.selected
Json Payload = {"option":"graphics","index":1}

Blueprint state to page

Blueprint emits:

Event Name   = player.stats.changed
Json Payload = {"health":85,"armor":20}

Page listener:

EngawaUIEvents.addListener("player.stats.changed", (stats) => {
  document.querySelector("#health").textContent = String(stats.health);
  document.querySelector("#armor").textContent = String(stats.armor);
});

Raw page message

engawa.postMessage({ type: "diagnostic", text: "Page mounted" });

Raw host message listener

window.addEventListener("engawa-message", ({ detail }) => {
  console.log("Unreal sent", detail);
});