bidi_domain.js

// Licensed to the Software Freedom Conservancy (SFC) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The SFC licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

const { getBidiConnection } = require('../lib/bidi_connection')

// Gates Domain's constructor so `new Network(someRandomThing)` fails loudly
// instead of silently producing a broken instance. A Symbol can't be forged
// or guessed, so this is real runtime enforcement, not just a TS annotation —
// only a generated `Class.create(driver)` (and this package's own tests) may
// pass it. It's exported deliberately, not hidden: the point is to stop
// accidental misuse of the normal `new Network(x)` shape, not to defend
// against someone who deliberately imports and passes this.
const DOMAIN_TOKEN = Symbol('Domain internal construction token — obtained only via Class.create(driver)')

/**
 * Describes one subscribable BiDi event, for use with Domain#addCallback().
 * @param {string} method
 * @param {{fromWire(payload: unknown): unknown}} [type] Runtime record/union
 *   class for the event's params, if the schema declares one. When present,
 *   addCallback() parses each delivered payload through it before the
 *   caller's handler runs — inbound wire payloads are validated against
 *   their resolved type; an event's params is such a payload just as much
 *   as a command's result is.
 * @returns {{method: string, type: ({fromWire(payload: unknown): unknown}|undefined)}}
 *   The event descriptor, ready to pass to Domain#addCallback().
 */
function event(method, type) {
  return { method, type }
}

/** Shared base for every generated BiDi domain class. See domain.d.ts for the typed surface. */
class Domain {
  #bidi

  constructor(bidi, token) {
    if (token !== DOMAIN_TOKEN) {
      throw new TypeError(`${new.target.name} must be constructed via ${new.target.name}.create(driver), not \`new\``)
    }
    this.#bidi = bidi
  }

  static async connect(driver) {
    return getBidiConnection(driver)
  }

  async send(method, params) {
    const response = await this.#bidi.send({ method, params })
    if (response?.error !== undefined) {
      throw new Error(`${response.error}: ${response.message}`)
    }
    return response?.result
  }

  /**
   * Subscribes `handler` to a BiDi event. All the actual subscription-lifecycle
   * work — remote subscribe/unsubscribe, per-subscription bookkeeping — lives on
   * the connection itself (see Index#addCallback in bidi/index.js); Domain only
   * adds the one thing the connection can't do on its own: parsing a delivered
   * payload through the descriptor's type before the caller's handler runs.
   * @param {{method: string, type: ({fromWire(payload: unknown): unknown}|undefined)}} descriptor
   *   An event descriptor from event().
   * @param {function(unknown): void} handler Invoked with the event's params
   *   (parsed through descriptor.type first, if one was given) each time it fires.
   * @returns {Promise<{id: string, unsubscribe: function(): Promise<void>}>}
   *   A handle for this subscription — call `unsubscribe()` to stop receiving the event.
   */
  async addCallback(descriptor, handler) {
    const dispatch = descriptor.type === undefined ? handler : (params) => handler(descriptor.type.fromWire(params))
    return this.#bidi.addCallback(descriptor.method, dispatch)
  }
}

module.exports = { Domain, event, DOMAIN_TOKEN }