BIDC (Bidirectional Channels for JavaScript)

repository·main·Indexed 22 days ago

https://github.com/vercel/bidc

A library for seamless, asynchronous, bidirectional communication between JavaScript execution contexts such as iframes, workers, and service workers. BIDC extends postMessage by supporting a custom streaming protocol that allows the transfer of complex data types (Map, Set, BigInt, Date), Promises, and Async Functions. It features automatic handshaking, connection re-establishment after reloads, and message buffering.

Tokens
2.7K
Snippets
7
Records
16
Agent score
79%

What's inside bidc

  1. Transfer Complex Data Types including Promises and Async Functions

    main

    BIDC supports a wide range of JavaScript types, including Date, RegExp, Map, Set, ArrayBuffer, TypedArray, and most importantly, Promise and Async Function.

    When you transfer a Promise, the receiving side can await it. When you transfer an Async Function, the receiving side can call it as if it were local, and the execution will occur in the original context.

    // Sending side
    const response = await send({
      date: new Date(),
      map: new Map([['key', 'value']]),
      set: new Set([1, 2, 3]),
      arrayBuffer: new Uint8Array([1, 2, 3]).buffer,
      promise: Promise.resolve('resolved value'),
      function: async (x) => {
        await sleep(1000)
        return x * 2
      }
    })
    
    // Receiving side
    receive(async (payload) => {
      const resolvedValue = await payload.promise
      const result = await payload.function(5)
      return { status: 'success' }
    })
  2. How BIDC bidirectional channels work

    main

    BIDC enables seamless communication between different JavaScript execution contexts (such as workers, iframes, or service workers) using a custom streaming protocol.

    Unlike standard postMessage APIs, BIDC:

    • Automatically establishes a handshake: It handles connection establishment and collision resolution regardless of which side initiates first.
    • Handles reloads: It automatically re-establishes connections if a context (like an iframe) is reloaded.
    • Buffers messages: It holds messages until the target side is ready to receive them.
    • Supports complex types: It allows transferring Promises, Async Functions, and complex data structures (Maps, Sets, etc.) as first-class citizens.

    To use it, you call createChannel(target) on one side and createChannel() (which defaults to the parent context) on the other.

    import { createChannel } from 'bidc'
    
    // On the parent side, targeting an iframe
    const { send } = createChannel(iframe.contentWindow)
    
    // Inside the iframe, targeting the parent
    const { receive } = createChannel()
  3. Send Promises and Complex Data Types via BIDC

    main

    BIDC supports sending asynchronous data and complex JavaScript types across the channel.

    Promise Serialization

    Promises are serialized and sent as separate chunks when they resolve. This allows you to send data that isn't immediately available.

    // This promise will be sent when it resolves
    delayedResponse: new Promise(resolve => 
      setTimeout(() => resolve(`Delayed response: "${message}"`), 2000)
    )

    Supported Complex Types

    The following serializable types are supported seamlessly:

    • Map
    • Set
    • BigInt
    • Date
    • Nested objects and arrays
    • Resolved Promises (Promise.resolve(...))
    data: {
      map: new Map([['key1', 'value1']]),
      set: new Set([1, 2, 3]),
      bigInt: BigInt(12345),
      date: new Date(),
      asyncData: Promise.resolve({ success: true })
    }
  4. Communicate with Web Workers

    main

    To use BIDC with a Web Worker, pass the worker instance to createChannel in the main thread. Inside the worker, calling createChannel() without arguments will automatically establish the channel back to the parent context.

    // Main Thread
    import { createChannel } from 'bidc'
    const worker = new Worker('./worker.js')
    const { send, receive } = createChannel(worker)
    
    // Inside worker.js
    import { createChannel } from 'bidc'
    const { send, receive } = createChannel()
  5. Establish BIDC channels in Parent and Iframe contexts

    main

    BIDC uses createChannel to establish bidirectional communication. The way you initialize the channel depends on whether the code is running in the parent window or within an iframe.

    • In the Parent Page: You must provide the target window (e.g., the iframe's contentWindow) to establish the connection. createChannel('channel-name', iframe.contentWindow)

    • In the Iframe Page: The library automatically detects the iframe context and uses window.parent for communication. You only need to provide the channel name. createChannel('channel-name')

  6. Setup the BIDC Next.js Demo

    main

    To run the BIDC Next.js demonstration application, follow these steps to install dependencies, build the core library, and start the development server.

    1. Install dependencies in the example directory:

      cd example
      npm install
      # or
      pnpm install
    2. Build the parent BIDC library from the repository root:

      cd ..
      pnpm build
    3. Run the development server in the example directory:

      cd example
      npm run dev
      # or
      pnpm dev
    4. Open the demo at http://localhost:3000.

  7. What is a SerializableValue?

    main

    A SerializableValue is any data type that can be safely encoded and transmitted across the BIDC channel. This includes standard primitives, complex objects, and even promises and functions.

    Supported types include:

    • Primitives: string, number, boolean, null, undefined, bigint
    • Objects/Arrays: SerializableObject, SerializableArray, Map, Set
    • Specialized: RegExp, Date, ArrayBuffer, TypedArray (e.g., Uint8Array)
    • Async: Promise<SerializableValue>
    • Functions: SerializableFunction (can be invoked remotely)
  8. How remote function calls work in BIDC

    main

    BIDC supports passing functions through the channel. When a function is sent, it is assigned a unique ID. When the receiver attempts to call this function, BIDC intercepts the call and sends a special message (bidc-fn:<id>) back to the sender. The sender executes the local function and returns the result via a response message (bidc-res:<id>).

    Note: Sending many anonymous or inline functions is discouraged as they cannot be cached and may increase the size of the internal function reference store.

  9. Basic Data Transfer between Parent and Iframe

    main

    You can send a message from a parent window to an iframe and await a response. The send method returns the value returned by the receive handler on the target side.

    Parent Window:

    import { createChannel } from 'bidc'
    const { send } = createChannel(iframe.contentWindow)
    const result = await send({ value: 'Hello, iframe!' })

    Inside the Iframe:

    import { createChannel } from 'bidc'
    const { receive } = createChannel()
    receive(payload => {
      return payload.value.toUpperCase()
    })
    // Parent
    import { createChannel } from 'bidc'
    const { send } = createChannel(iframe.contentWindow)
    const result = await send({ value: 'Hello, iframe!' })
    
    // Iframe
    import { createChannel } from 'bidc'
    const { receive } = createChannel()
    receive(payload => {
      return payload.value.toUpperCase()
    })
  10. Two-Way Communication with send and receive

    main

    Both sides of a BIDC channel can implement both send and receive to create a full duplex communication loop.

    // Parent
    import { createChannel } from 'bidc'
    const { send, receive } = createChannel(iframe.contentWindow)
    
    receive((payload) => {
      return { response: 'Message received!' }
    })
    
    const responseFromIframe = await send({ value: 'Hello, iframe!' })
    
    // Iframe
    import { createChannel } from 'bidc'
    const { send, receive } = createChannel()
    
    receive((payload) => {
      return { response: 'Hello, parent!' }
    })
    
    const responseFromParent = await send({ value: 'Hello, parent!' })
  11. Create Namespaced Channels

    main
    To avoid collisions when multiple components need to communicate over the same connection (e.g., between a parent and an iframe), use the second argument of createChannel to specify a namespace ID. The ID must match on both sides.