For AI agents: the complete documentation index is available at /next/llms.txt, the full documentation bundle is available at /next/llms-full.txt, and this page is available as Markdown at /next/lynxtron/learn/Communication-Between-Node-And-Lynx.md.
  • English
  • Communication Between Node.js and Lynx in Lynxtron

    If you are familiar with Electron, you can first understand Lynxtron communication needs in two categories:

    1. Bidirectional message communication between the Node main process and UI: similar to IPC between Electron's main process and a BrowserWindow renderer.
    2. Using Node.js capabilities directly in UI code: Electron technically supports nodeIntegration for calling Node.js capabilities directly in Web code, but enabling it directly is generally not recommended. Lynxtron instead enhances preload scripts so they can use Node.js in an isolated context and expose JS objects for Lynx UI to call.

    Lynxtron provides corresponding capabilities for both needs:

    Electron Capability Mapping

    NeedElectronLynxtron
    UI uses Node.js capabilitiespreload scripts / contextBridge.exposeInMainWorld() / nodeIntegrationlynxPreference.preload + contextBridge.exposeInLynxBTS() + NativeModules.nodejs.exposed
    Node main process pushes messages to UIwebContents.send()LynxWindow.sendGlobalEvent()
    UI calls Node main process and waits for a resultipcRenderer.invoke() + ipcMain.handle()NativeModules.bridge.call() + lynxBridge.handle()
    UI sends one-way messages to Node main processipcRenderer.send() + ipcMain.on()NativeModules.bridge.send() + lynxBridge.on()

    Use Preload Scripts

    Lynxtron's preload scripts can be understood as an enhancement of Electron preload scripts / contextBridge model for Lynx BTS: a preload script can use Node.js APIs directly and expose methods or objects for Lynx UI to call.

    In Electron, the preload script and the Web page are in isolated JS contexts. The preload script and Web eventually communicate across contexts through contextBridge or IPC, with values serialized, proxied, or copied according to Electron's rules; the Web side cannot directly use the original JS objects from the preload script context.

    In Lynxtron, Lynx BTS and preload scripts both run in Node.js, but in isolated JS contexts. The preload script context has Node.js capabilities and can expose JS objects through contextBridge.exposeInLynxBTS() for Lynx BTS to access directly.

    Configure Preload Scripts

    When creating a window, specify the preload script through lynxPreference.preload:

    // main.ts
    import { app, LynxWindow } from '@lynx-js/lynxtron';
    import path from 'path';
    
    app.whenReady().then(() => {
      const win = new LynxWindow({
        width: 800,
        height: 600,
        lynxPreference: {
          preload: path.join(__dirname, 'preload.js'),
        },
      });
    
      win.loadFile('main.lynx.bundle');
    });

    Expose Node.js APIs from a Preload Script

    The preload script can use Node.js APIs directly, then expose them to Lynx with contextBridge.exposeInLynxBTS():

    // preload.ts
    import { contextBridge } from '@lynx-js/lynxtron/context-bridge';
    import fs from 'fs';
    import path from 'path';
    
    type ExposedNodeApi = {
      readTextFileWithTransform: (
        filePath: string,
        transform: (content: string) => string,
      ) => Promise<string>;
      createReader: (baseDir: string) => {
        read: (relativePath: string) => Promise<string>;
      };
    };
    
    const nodeApi: ExposedNodeApi = {
      readTextFileWithTransform: async (filePath, transform) => {
        const content = await fs.promises.readFile(filePath, 'utf8');
        return transform(content);
      },
    
      createReader: (baseDir: string) => {
        return {
          read: (relativePath: string) => {
            return fs.promises.readFile(path.join(baseDir, relativePath), 'utf8');
          },
        };
      },
    };
    
    contextBridge.exposeInLynxBTS(nodeApi);

    Call Exposed APIs from Lynx

    Lynx BTS accesses these APIs through NativeModules.nodejs.exposed:

    The exposed JS object can contain functions. Functions can take other functions as parameters, use closures from the preload script context, and return Promises. Lynx UI can call them as regular asynchronous JS APIs.

    // App.tsx
    const api = NativeModules.nodejs.exposed;
    
    const title = await api.readTextFileWithTransform(
      '/Users/example/notes.txt',
      (text) => text.split('\n')[0],
    );
    
    const reader = api.createReader('/Users/example');
    const todo = await reader.read('todo.txt');

    Example: File Explorer

    This example demonstrates using Node.js capabilities exposed by a preload script in Lynx UI:

    Bidirectional Message Communication Between Node Main Process and Lynx

    Use this option for window control, system services, database access, main-process state management, and similar tasks. Node.js capabilities stay in the main process, while Lynx calls them through messages.

    Push Messages from Node.js to Lynx

    The Node main process can send events to Lynx with LynxWindow.sendGlobalEvent():

    // main.ts
    import { LynxWindow } from '@lynx-js/lynxtron';
    
    const win = new LynxWindow();
    
    win.sendGlobalEvent('systemInfoUpdate', {
      cpuUsage: 32,
      memoryUsage: 68,
    });

    The Lynx side listens with GlobalEventEmitter:

    // App.tsx
    import { useEffect, useState } from '@lynx-js/react';
    
    export function App() {
      const [systemInfo, setSystemInfo] = useState(null);
    
      useEffect(() => {
        const emitter = lynx.getJSModule('GlobalEventEmitter');
        const handleUpdate = (data) => {
          setSystemInfo(data);
        };
    
        emitter.addListener('systemInfoUpdate', handleUpdate);
        return () => {
          emitter.removeListener('systemInfoUpdate', handleUpdate);
        };
      }, []);
    
      return <view>{/* render systemInfo */}</view>;
    }

    Call Node.js from Lynx and Wait for a Result

    Use bridge.call to make the call, since you need to wait for Node.js to return data.

    // App.tsx
    const info = await NativeModules.bridge.call('getSystemInfo', {
      includeCpuDetail: true,
    });
    setSystemInfo(info);

    The Node main process registers a handler with lynxBridge.handle() to process bridge.call invocations. The return value (or a resolved Promise) is sent back to Lynx automatically, similar to ipcMain.handle() in Electron:

    // main.ts
    import { app, lynxBridge } from '@lynx-js/lynxtron';
    
    app.whenReady().then(() => {
      lynxBridge.handle('getSystemInfo', (event, params) => {
        return getSystemInfo(params);
      });
    });

    For asynchronous work, return a Promise and let the runtime send the reply when it resolves. Do not call event.sendReply() from a handler registered with handle: the runtime already invokes sendReply once with the handler's resolved value, so an explicit call would produce a second, out-of-order reply.

    lynxBridge.handle('getSystemInfo', (event, params) => {
      return new Promise<SystemInfo>((resolve) => {
        fetchAsync(params, resolve);
      });
    });

    Send One-Way Messages from Lynx to Node.js

    Notify Node.js with bridge.send (one-way, no return value needed).

    // App.tsx
    NativeModules.bridge.send('refreshIntervalChanged', { interval: 1000 });

    The Node main process listens for messages on lynxBridge (which is a Node.js EventEmitter). This mirrors ipcMain.on() in Electron:

    // main.ts
    import { app, lynxBridge } from '@lynx-js/lynxtron';
    
    app.whenReady().then(() => {
      lynxBridge.on('refreshIntervalChanged', (params) => {
        console.log('refresh interval:', params.interval);
      });
    });

    lynxBridge API

    lynxBridge is a main-process singleton imported from @lynx-js/lynxtron. It is a Node.js EventEmitter, so it exposes the standard on / once / off / emit methods in addition to the invoke helpers below. In Electron terms, lynxBridge plays the role of ipcMain.

    MethodDescription
    lynxBridge.handle(method, handler)Registers a handler for NativeModules.bridge.call(method, args). The handler's return value (or a resolved Promise) is sent back to Lynx. Registering the same method twice throws.
    lynxBridge.handleOnce(method, handler)Same as handle, but the handler is removed after being called once.
    lynxBridge.removeHandler(method)Removes a handler previously registered with handle / handleOnce.
    lynxBridge.on(channel, listener)Inherited from EventEmitter. Receives one-way NativeModules.bridge.send(channel, args) messages from Lynx. The listener is called with (args).

    The handler signature is (event: LynxBridgeInvokeEvent, args) => unknown | Promise<unknown>. The runtime automatically calls event.sendReply() once with the handler's resolved value, so handlers registered through handle / handleOnce should express the reply as their return value (or a Promise). Manually invoking event.sendReply() from such a handler produces a duplicate reply. sendReply is intended for low-level callers that subscribe to the internal invoke event directly instead of going through handle.

    Example: System Resource Monitor

    This example demonstrates bidirectional message communication between the Node main process and Lynx:

    • The Node.js side uses the os module to get CPU, memory, and other system information, then pushes updates to Lynx with LynxWindow.sendGlobalEvent().
    • The Lynx side receives updates through GlobalEventEmitter, and can also actively request the latest data with NativeModules.bridge.call().
    • Notifications that do not need a return value, such as refresh interval changes, can be sent to the Node main process with NativeModules.bridge.send().

    Capability Reference

    CapabilityAPI
    Configure the window-level preload scriptlynxPreference.preload
    Expose Node.js methods or objects from preload scripts to LynxcontextBridge.exposeInLynxBTS()
    Call Node.js methods or objects exposed by preload scripts from Lynx UINativeModules.nodejs.exposed
    Push events from the Node main process to LynxLynxWindow.sendGlobalEvent()
    Call the Node main process from Lynx and wait for a resultNativeModules.bridge.calllynxBridge.handle()
    Send one-way messages from Lynx to the Node main processNativeModules.bridge.sendlynxBridge.on()

    Why Lynxtron Is Designed This Way

    Lynxtron uses two channels to cover the two common Electron capabilities because they solve different problems:

    • NativeModules.nodejs.exposed: direct access from Lynx BTS to JS objects exposed by preload scripts, corresponding to using Node.js capabilities in UI through Electron preload scripts / nodeIntegration.
    • NativeModules.bridge + lynxBridge: message communication between Lynx and the Node main process, corresponding to Electron's ipcRendereripcMain IPC.

    The key difference is not which global object the API is mounted on, but that Lynxtron provides a JS-object exposure path for Lynx BTS.

    API / SpecDescription
    LynxWindowCreates windows and loads Lynx bundles
    LynxWindowConstructorOptionsConfigures window options such as lynxPreference
    contextBridge.exposeInLynxBTS()Exposes JS objects from preload scripts to Lynx BTS
    LynxWindow.sendGlobalEvent()Pushes global events from the Node main process to Lynx
    lynxBridgeMain-process singleton for handling bridge.call / bridge.send from Lynx
    LynxBridgeInterface of lynxBridge; exposes handle / handleOnce / removeHandler and EventEmitter API
    LynxBridgeInvokeEventEvent object passed to lynxBridge.handle(); exposes sendReply()
    LynxtronNodejsType of NativeModules.nodejs
    GlobalEventEmitterListens for global events in Lynx BTS
    lynx.getJSModule()Gets modules such as GlobalEventEmitter
    Except as otherwise noted, this work is licensed under a Creative Commons Attribution 4.0 International License, and code samples are licensed under the Apache License 2.0.