Skip to content

Stream bytes between the page and Java

fakeivchenko edited this page Sep 29, 2026 · 1 revision

A binding answers with one value. For bytes, for large answers, and for answers that arrive over time, a page makes a call with lwjwae.call, which resolves to a Response, as fetch does, and Java answers it with handle. In this guide, a report streams into the page line by line, the page sends 64 MB to Java, and a value goes both ways through the codec.

A streaming report, an upload, and a value, on Linux and on Windows 11

Answer calls in Java

public class Streams {
  public static void main(String[] args) {
    Application.launch(
        WindowParameters.of("Streams", 640, 520),
        window -> {
          window.handle("report", Streams::report);
          window.handle("checksum", Streams::checksum);
          window.handle("summary", Streams::summary);
          window.loadResource("streams/index.html");
        });
  }

  /** Streams one line after another, until the report is done or the page cancels it. */
  private static void report(RpcCall call) throws Exception {
    int lines = Integer.parseInt(call.text());
    try (RpcStream stream = call.stream("text/plain")) {
      for (int line = 1; line <= lines; line++) {
        String text = "Line " + line + " of " + lines + "\n";
        if (!stream.write(text.getBytes(StandardCharsets.UTF_8))) {
          return; // The page cancelled the call.
        }
        Thread.sleep(2);
      }
    }
  }

  /** Answers the bytes that the page sent with their SHA-256. */
  private static void checksum(RpcCall call) throws Exception {
    byte[] digest = MessageDigest.getInstance("SHA-256").digest(call.body());
    call.reply(HexFormat.of().formatHex(digest));
  }

  /** Takes a task through the codec, and answers a value through it. */
  private static void summary(RpcCall call) {
    Task task = call.value(Task.class);
    call.replyValue(new Task(task.title().toUpperCase(), task.done()));
  }
}

An RpcCall has what the page sent, as body(), text(), or value(Class), and one way to answer:

Answer Method
Text reply(String)
Bytes of a type reply(byte[], String contentType)
A file among the resources replyResource(String)
A value through the codec replyValue(Object)
A stream stream(String contentType), then write(byte[]) for each part

write blocks while the page is behind, and answers false once the page gave up on the call, which is the signal to stop. A handler that throws answers with an error that rejects the promise on the page.

Make calls from the page

let controller = null;

// A stream: the answer is a Response, and its body arrives while Java writes it.
document.querySelector("#report").addEventListener("click", async () => {
  controller = new AbortController();
  const lines = document.querySelector("#lines");
  lines.textContent = "";
  try {
    const response = await lwjwae.call("report", "2000", { signal: controller.signal });
    const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
    for (let chunk = await reader.read(); !chunk.done; chunk = await reader.read()) {
      lines.textContent += chunk.value;
    }
  } catch (error) {
    if (error.name !== "AbortError") throw error;
    lines.textContent += "Cancelled\n";
  }
});

document.querySelector("#cancel").addEventListener("click", () => controller?.abort());

// Bytes up: the body can be an ArrayBuffer, a typed array, a Blob, or a string.
document.querySelector("#upload").addEventListener("click", async () => {
  const bytes = new Uint8Array(64 * 1024 * 1024).map((_, index) => index % 251);
  const response = await lwjwae.call("checksum", bytes);
  document.querySelector("#checksum").textContent = `SHA-256 ${await response.text()}`;
});

// A value both ways through the codec, without a Response.
document.querySelector("#summary").addEventListener("click", async () => {
  const task = await lwjwae.invoke("summary", { title: "Write the guide", done: true });
  document.querySelector("#value").textContent = JSON.stringify(task);
});

lwjwae.call(name, body, { signal }) resolves to a Response: text(), json(), arrayBuffer(), blob(), or body to read it while it arrives. Aborting the signal cancels the call on both sides. lwjwae.invoke(name, value) is the short form for a value through the codec, which needs a codec as in Pass objects through the bridge.

Good to know

  • Only the pages of the application, and the development server, may call. A page of another site that the window shows can't.
  • A handler of the Application rather than of a Window answers the calls of every window; call.window() tells which one called.
  • Calls are fast: 64 MB reach Java in a fraction of a second, as the screenshots show, so a page can hand whole files to Java rather than paths.

Clone this wiki locally