Contents

MōBrowser 2.8.0

We’re happy to announce MōBrowser 2.8.0. This release adds streaming IPC from main to renderer, two-way TypeScript–C++ calls, transparent window background on Windows, bundling third-party resources, singing native Node.js modules, and more.

What’s new 

Streaming IPC from main to renderer 

In addition to unary calls, the renderer ↔ main IPC channel now supports server-side streaming. A method declared with the stream keyword in a .proto file generates a renderer-side Stream<T> that the main process can push messages onto at any time — ideal for download progress, theme changes, notifications, log tailing, AI completions, or anything else the main process emits over time.

Declare a streaming method on a service:

// src/renderer/proto/files.proto
syntax = "proto3";
import "google/protobuf/empty.proto";

message FileChanged { string path = 1; }

service FilesService {
  rpc OnFileChanged(google.protobuf.Empty) returns (stream FileChanged);
}

In the main process, declare the publisher once and push messages whenever they’re produced — on an event listener, a timer, anywhere. When no renderer is subscribed the call is a cheap no-op; otherwise the message is encoded once and fanned out to every subscriber:

import { watch } from 'node:fs';
import { ipc } from '@mobrowser/api';
import { FilesServiceDescriptor } from './gen/ipc_service';

const files = ipc.registerService(FilesServiceDescriptor);

watch('./data', (event, file) => {
  if (event === 'rename' && file) files.OnFileChanged({ path: file });
});

The generated renderer client returns a Stream<T> that integrates flawlessly with every major UI framework — it satisfies the TC39 Observable contract, Svelte’s store contract, and [Symbol.asyncIterator] simultaneously, so React, Vue, Svelte, Angular, and RxJS accept it as-is. Here’s how it looks in React and Vue:

// React
React.useEffect(() => {
  const sub = ipc.files.OnFileChanged({}).subscribe({
    next: setFile,
  });
  return () => sub.unsubscribe();
}, []);
<!-- Vue -->
<script setup lang="ts">
import { useObservable } from '@vueuse/rxjs';

const file = useObservable(ipc.files.OnFileChanged({}));
</script>

<template>
  <p v-if="file">Last change: {{ file.path }}</p>
</template>

For request-driven streams where each subscription owns its resource — an AI session, a streaming HTTP request, a paginated DB query — implement the handler as an async generator instead. The request shapes the stream, and ctx.signal plumbs straight into any abortable upstream so a renderer disconnect immediately collapses into upstream cancellation:

import OpenAI from 'openai';
import { ipc } from '@mobrowser/api';
import { ChatServiceDescriptor } from './gen/ipc_service';

const openai = new OpenAI();

ipc.registerService(ChatServiceDescriptor, {
  async *StreamChat(req, ctx) {
    const completion = await openai.chat.completions.create(
      { model: 'gpt-4o-mini',
        messages: [{ role: 'user', content: req.prompt }],
        stream: true },
      { signal: ctx.signal },
    );
    for await (const chunk of completion) {
      const text = chunk.choices[0]?.delta?.content;
      if (text) yield { text };
    }
  },
});

The renderer drains the stream with for await. break tears the underlying subscription down through the same path as unsubscribe() — and the abort travels all the way back to the OpenAI request:

for await (const chunk of ipc.chat.StreamChat({ prompt })) {
  appendText(chunk.text);
  if (userClickedStop()) break; // cancels the upstream OpenAI request
}

unsubscribe(), break from for await, an aborted AbortSignal, and frame unload all collapse to one cancellation chain — no per-call cleanup boilerplate.

Structured IPC errors 

Both unary and streaming RPCs now surface failures as a single structured IpcError, exported from @mobrowser/api:

class IpcError extends Error {
  readonly name: 'IpcError';
  readonly code: string;
  readonly details?: unknown;
}

Throw one from any handler with a typed code and arbitrary JSON-serializable details:

throw new IpcError({
  code: 'PERMISSION_DENIED',
  message: 'Downloads are disabled by policy',
  details: { requestedUrl: req.url, reason: 'managed-policy' },
});

The renderer catches a real IpcError instance — e instanceof IpcError, e.code, and e.details all work without per-call boilerplate:

import { IpcError } from '@mobrowser/api/streams';

try {
  await ipc.downloads.StartDownload({ url });
} catch (e) {
  if (e instanceof IpcError && e.code === 'PERMISSION_DENIED') {
    showUpsell(e.details);
    return;
  }
  throw e;
}

Any error value with a .code field works — Node’s fs errors, pg driver errors, and friends — the runtime duck-types code, message, and details on the way out. For streams, a thrown handler surfaces as a rejected iter.next() (for for await) or obs.error(e) (for subscribe). Renderer disconnects are not errors: subscribers cleanly resolve with { done: true }.

Calling TypeScript from C++ 

You can now make calls between TypeScript (main process) and C++ (native module) in both directions. In the previous version of the framework, you could only call C++ functions from TypeScript. Now you can call TypeScript functions from C++ and vice versa.

Here’s an example of how to call a TypeScript function from C++.

In the TypeScript code (main process), you need to implement the service by inheriting from the generated service classes and register it using the native.registerService() function:

import { native } from './gen/native';
import { GreetServiceDescriptor } from './gen/native_service';
import { Person } from './gen/native/greet';

native.registerService(GreetServiceDescriptor, {
  async SayHello(person: Person) {
    return { value: `Hello, ${person.name}!` };
  },
})

In the C++ code of the native module, you can call the TypeScript service from C++ code:

#include <iostream>
#include "gen/greet.rpc.h"

void launch() {
  Person person;
  person.set_name("John Doe");
  mo::rpc::greet.SayHello(person, [](mo::rpc::Result<StringValue> result) {
    if (auto value = result.value()) {
      std::cout << value->value() << std::endl;
    }
  });
}

Transparent window on Windows 

We added support for transparent window backgrounds on Windows.

import { BrowserWindow } from '@mobrowser/api';

const win = new BrowserWindow()
win.setTransparentBackground(true)
win.show()

Bundling third-party resources 

You can now bundle third-party resources with your application using the extras property in the mobrowser.conf.json file. This is useful when you want to ship additional files with your application like images, fonts, or other resources that are not part of the application code.

Readable stack traces 

All the unhandled errors in the main process will be displayed in the console output window with the call stack. The framework now enables the source maps support for the main process, so you can see the original source code and valid line numbers in the call stack instead of the compiled JavaScript code.

Here’s an example of the error call stack you will now see in the console output:

Error: Simulated error for source map stack trace check.
    at i (/app/src/main/index.ts:5:13)
    at a (/app/src/main/index.ts:10:3)

In the previous version of the framework, you would see the call stack with the file paths that point to the compiled JavaScript code instead of the original source code.

Error: Simulated error for source map stack trace check.
    at i (file:///app/out/main/index.js:22:8)
    at a (file:///app/out/main/index.js:25:27)

Detecting Node.js version mismatch 

An attempt to use a third-party Node.js module that is not compatible with the version of Node.js used by MōBrowser will now result in an error with a detailed explanation instead of a crash. Here’s what you will see in the terminal if you try to use the sqlite3 module that is not compatible with the version of Node.js used by MōBrowser:

Error: Cannot load "/app/node_modules/better-sqlite3/build/Release/better_sqlite3.node":
       the native module was compiled against a different Node.js version.
The module '/app/node_modules/better-sqlite3/build/Release/better_sqlite3.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION 115. This version of Node.js requires
NODE_MODULE_VERSION 137. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).
Rebuild the module against MoBrowser's bundled Node.js.

Signing native Node modules 

When you add a Node module dependency with a native Node.js module to your project, MōBrowser will now automatically bundle the native Node.js module *.node files into your application and sign it if it’s not already signed.

Fixes 

  • Removed the white flickering when displaying the browser window on macOS.
  • Fixed the empty View Source window size.
  • Disabled the default Chromium shortcuts that can lead to unexpected behavior like opening the Download History window when pressing Cmd+Shift+L on macOS.