penpal

repository·main·Indexed 20 days ago

https://github.com/aaronius/penpal

A library that simplifies cross-context communication between iframes, workers, and windows by providing a promise-based API on top of the browser's postMessage mechanism. Version 7.0.6 supports WindowMessenger, WorkerMessenger, and PortMessenger for various communication scenarios, including Service Workers and Shared Workers. It features TypeScript support, connection and method call timeouts, and the ability to transfer large data using transferable objects.

Tokens
9.1K
Snippets
38
Records
42
Agent score
68%

What's inside penpal

  1. Transfer Large Data using Transferable Objects

    main

    By default, Penpal uses the structured clone algorithm, which copies data and can double memory usage. To improve performance when sending large buffers, use transferable objects to move memory ownership instead of cloning.

    From Caller to Remote

    Pass an instance of CallOptions as the last argument to a remote method, setting the transferables option to an array of the objects you wish to transfer (e.g., ArrayBuffers).

    From Remote to Caller

    When responding to a method call, return an instance of Reply instead of a raw value. Set the transferables option in the Reply constructor to specify which objects should be transferred back to the caller.

    // Caller side: transferring a buffer to the remote
    const numbersArray = new Int32Array(new ArrayBuffer(8));
    const result = await remote.double(
      numbersArray, 
      new CallOptions({ transferables: [numbersArray.buffer] })
    );
    
    // Remote side: returning a buffer via Reply
    // methods: { double(numbersArray) { ... return new Reply(resultArray, { transferables: [resultArray.buffer] }); } }
  2. Use Channels for Parallel Connections

    main

    If you need to establish multiple simultaneous connections between the same two participants (e.g., two different connections between a Parent and an Iframe), you must use channels to disambiguate messages.

    Provide a unique string channel identifier in the connect() options for each connection on both sides. Each connection must also use its own unique messenger instance.

    // Parent Window
    const connectionA = connect({
      messenger: messengerA,
      channel: 'A',
      methods: { ... }
    });
    
    const connectionB = connect({
      messenger: messengerB,
      channel: 'B',
      methods: { ... }
    });
    
    // Iframe Window
    const connectionA = connect({
      messenger: messengerA,
      channel: 'A',
      methods: { ... }
    });
    
    const connectionB = connect({
      messenger: messengerB,
      channel: 'B',
      methods: { ... }
    });
  3. Communicate with a Web Worker

    main

    Use WorkerMessenger to establish communication between a Window and a Web Worker.

    Window Setup:

    • Instantiate a Worker (e.g., new Worker('worker.js')).
    • Create a WorkerMessenger passing the worker instance.
    • Use connect() to expose methods to the worker.

    Worker Setup:

    • Create a WorkerMessenger passing self as the worker.
    • Use connect() to expose methods to the window.
    // Window Side
    import { WorkerMessenger, connect } from 'penpal';
    
    const worker = new Worker('worker.js');
    const messenger = new WorkerMessenger({ worker });
    const connection = connect({
      messenger,
      methods: {
        add(num1, num2) {
          return num1 + num2;
        },
      },
    });
    
    const remote = await connection.promise;
  4. Communicate with a Shared Worker

    main

    Use PortMessenger to communicate with a SharedWorker via its port.

    Window Setup:

    • Instantiate a SharedWorker.
    • Create a PortMessenger passing worker.port.
    • Use connect() to expose methods to the worker.

    Shared Worker Setup:

    • Listen for the 'connect' event on the worker.
    • Extract the port from event.ports.
    • Create a PortMessenger using that port.
    • Use connect() to expose methods to the window.
    // Window Side
    import { PortMessenger, connect } from 'penpal';
    
    const worker = new SharedWorker('shared-worker.js');
    const messenger = new PortMessenger({ port: worker.port });
    const connection = connect({
      messenger,
      methods: {
        add(num1, num2) {
          return num1 + num2;
        },
      },
    });
    
    const remote = await connection.promise;
  5. Use TypeScript with Penpal

    main

    Penpal provides full TypeScript support. To get type safety for your remote calls, pass a generic type argument to connect() that defines the interface of the remote methods.

    Typing the Remote Proxy

    When you call connect<T>({ messenger }), the connection.promise resolves to a RemoteProxy<T>, allowing your IDE to provide autocomplete and type checking for the methods available on the remote object.

    Worker Type Declarations

    When working with Workers, add a global type declaration at the top of your worker script to help TypeScript understand the environment:

    • Dedicated Worker: declare const self: DedicatedWorkerGlobalScope;
    • Shared Worker: declare const self: SharedWorkerGlobalScope;
    • Service Worker: declare const self: ServiceWorkerGlobalScope;
    import { WorkerMessenger, connect } from 'penpal';
    
    interface WorkerApi {
      multiply(...args: number[]): number;
    }
    
    const worker = new Worker('worker.js');
    const messenger = new WorkerMessenger({ worker });
    
    // Pass WorkerApi as a generic to connect()
    const connection = connect<WorkerApi>({ messenger });
    
    const remote = await connection.promise;
    const result = await remote.multiply(2, 6); // Properly typed
  6. Debug Penpal connections

    main

    You can debug Penpal communication by providing a log function to the connect() options. Penpal exports a debug utility that can be used to prefix log messages, making it easier to identify which side (e.g., 'parent' or 'child') is logging.

    import { connect, debug } from 'penpal';
    
    const connection = connect({
      messenger,
      log: debug('parent')
    });
  7. Communicate with an Iframe

    main

    Use WindowMessenger to establish a promise-based connection between a parent window and an iframe.

    Parent Window Setup:

    1. Create a WindowMessenger providing the iframe.contentWindow as the remoteWindow.
    2. Specify allowedOrigins to restrict communication to trusted domains.
    3. Call connect() with the messenger and a methods object defining what the iframe can call.

    Iframe Window Setup:

    1. Create a WindowMessenger providing window.parent as the remoteWindow.
    2. Call connect() with the messenger and a methods object defining what the parent can call.

    In both cases, await connection.promise returns a remote object. Calling any method on remote returns a Promise.

    // Parent Window Example
    import { WindowMessenger, connect } from 'penpal';
    
    const iframe = document.createElement('iframe');
    iframe.src = 'https://childorigin.example.com/path/to/iframe.html';
    document.body.appendChild(iframe);
    
    const messenger = new WindowMessenger({
      remoteWindow: iframe.contentWindow,
      allowedOrigins: ['https://childorigin.example.com'],
    });
    
    const connection = connect({
      messenger,
      methods: {
        add(num1, num2) {
          return num1 + num2;
        },
      },
    });
    
    const remote = await connection.promise;
    const multiplicationResult = await remote.multiply(2, 6);
  8. Communicate with an Opened Window

    main

    Use WindowMessenger to communicate with a window opened via window.open().

    Parent Window Setup:

    • Use the object returned by window.open() as the remoteWindow in WindowMessenger.

    Opened Window Setup:

    • Use window.opener as the remoteWindow in WindowMessenger.

    Methods exposed via the methods object in connect() are accessible on the remote object returned by await connection.promise.

    // Parent Window Example
    import { WindowMessenger, connect } from 'penpal';
    
    const windowUrl = 'https://childorigin.example.com/path/to/window.html';
    const childWindow = window.open(windowUrl);
    
    const messenger = new WindowMessenger({
      remoteWindow: childWindow,
      allowedOrigins: ['https://childorigin.example.com'],
    });
    
    const connection = connect({
      messenger,
      methods: {
        add(num1, num2) {
          return num1 + num2;
        },
      },
    });
    
    const remote = await connection.promise;
  9. Configure Connection and Method Call Timeouts

    main

    You can prevent indefinite waiting by specifying timeouts in milliseconds.

    Connection Timeouts

    When calling connect(), provide a timeout property in the options object. If the connection isn't established within this period, connection.promise will reject with ErrorCode.ConnectionTimeout.

    Method Call Timeouts

    When invoking a remote method, pass an instance of CallOptions as the final argument. If the remote does not respond within the specified time, the method call promise will reject with ErrorCode.MethodCallTimeout.

    import { connect, CallOptions, ErrorCode } from 'penpal';
    
    // Connection timeout
    const connection = connect({
      messenger,
      timeout: 5000 // 5 seconds
    });
    
    try {
      const remote = await connection.promise;
      
      // Method call timeout
      const result = await remote.multiply(2, 6, new CallOptions({ timeout: 1000 }));
    } catch (error) {
      if (error.code === ErrorCode.ConnectionTimeout) {
        // Handle connection timeout
      } else if (error.code === ErrorCode.MethodCallTimeout) {
        // Handle method call timeout
      }
    }
  10. Install Penpal via CDN

    main

    Load Penpal directly in the browser using a CDN. When using the CDN, Penpal is available on the global window.Penpal object. Instead of importing specific modules, access them via Penpal (e.g., Penpal.connect, Penpal.WindowMessenger).

    <script src="https://unpkg.com/penpal@^7/dist/penpal.min.js"></script>
  11. Communicate with a Service Worker

    main

    Use PortMessenger and MessageChannel to bridge communication between a Window and a Service Worker.

    Window Setup:

    1. Create a new MessageChannel.
    2. Send port2 to the Service Worker via navigator.serviceWorker.controller.postMessage, including a custom type (e.g., 'INIT_PENPAL') and the port in the transfer array.
    3. Create a PortMessenger using port1.
    4. Use connect() to expose methods to the worker.

    Service Worker Setup:

    1. Listen for the 'message' event.
    2. Check for the custom type (e.g., 'INIT_PENPAL').
    3. Extract the port from the message data.
    4. Create a PortMessenger using that port.
    5. Use connect() to expose methods to the window.
    // Window Side
    import { PortMessenger, connect } from 'penpal';
    
    const initPenpal = async () => {
      const { port1, port2 } = new MessageChannel();
    
      navigator.serviceWorker.controller?.postMessage(
        { type: 'INIT_PENPAL', port: port2 },
        { transfer: [port2] }
      );
    
      const messenger = new PortMessenger({ port: port1 });
      const connection = connect({
        messenger,
        methods: {
          add(num1, num2) { return num1 + num2; },
        },
      });
    
      const remote = await connection.promise;
    };