 function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }import {
  asMaybe,
  asNumber,
  asObject,
  asOptional,
  asString,
  asUnknown,
  asValue,

  uncleaner
} from 'cleaners'

/**
 * A codec object can make calls to the remote system,
 * and can process incoming messages from the remote system.
 */






















































































/**
 * Accepts cleaners for the two sides of a protocol,
 * and returns a codec factory.
 */
export function makeRpcProtocol


(
  opts
) {
  const {
    serverMethods,
    clientMethods,
    asCall = asJsonRpcCall,
    asReturn = asJsonRpcReturn,
    allowMultipleReturns = false
  } = opts

  return {
    makeServerCodec(opts) {
      return makeCodec(
        serverMethods,
        clientMethods,
        asCall,
        asReturn,
        allowMultipleReturns,
        opts
      )
    },
    makeClientCodec(opts) {
      return makeCodec(
        clientMethods,
        serverMethods,
        asCall,
        asReturn,
        allowMultipleReturns,
        opts
      )
    }
  }
}

















function makeCodec(
  localCleaners,
  remoteCleaners,
  asCall,
  asReturn,
  allowMultipleReturns,
  opts
) {
  const { handleError, handleSend, localMethods } = opts
  const wasCall = uncleaner(asCall)
  const wasReturn = uncleaner(asReturn)

  const sendError = async (
    code,
    message,
    id = null
  ) => {
    const payload = wasReturn({
      jsonrpc: '2.0',
      result: undefined,
      error: { code, message, data: undefined },
      id
    })
    return await handleSend(JSON.stringify(payload))
  }

  // Remote state:
  let nextRemoteId = 0
  const remoteCalls = new Map()
  const remoteSubscriptions = new Map()

  // Create proxy functions for each remote method:
  const remoteMethods

 = {}
  for (const methodName of Object.keys(remoteCleaners)) {
    const { asParams, asResult } = remoteCleaners[methodName]
    const wasParams = uncleaner(asParams)

    if (asResult == null) {
      // It's a notification, so send the message with no result handling:
      remoteMethods[methodName] = (params) => {
        const payload = wasCall({
          jsonrpc: '2.0',
          method: methodName,
          params: wasParams(params)
        })
        handleSend(JSON.stringify(payload)).catch(handleError)
      }
    } else {
      // It's a method call, so sign up to receive a result:
      remoteMethods[methodName] = (params) => {
        const id = nextRemoteId++
        const out = new Promise((resolve, reject) => {
          remoteCalls.set(id, {
            asResult,
            resolve,
            reject
          })
        })

        // This is a subscription request:
        if (
          allowMultipleReturns &&
          localCleaners[methodName] != null &&
          localCleaners[methodName].asResult == null
        ) {
          // Keep track of the remote method name to send notifications
          remoteSubscriptions.set(id, methodName)
        }

        const request = wasCall({
          jsonrpc: '2.0',
          method: methodName,
          params: wasParams(params),
          id
        })
        handleSend(JSON.stringify(request)).catch(handleError)

        return out
      }
    }
  }

  function handleMessage(message) {
    let json
    try {
      json = JSON.parse(message)
    } catch (error) {
      sendError(-32700, `Parse error: ${errorMessage(error)}`).catch(
        handleError
      )
      return
    }

    // TODO: We need to add support for batch calls.
    const call = asMaybe(asCall)(json)
    const response = asMaybe(asReturn)(json)

    if (call != null) {
      const cleaners = localCleaners[call.method]
      if (cleaners == null) {
        sendError(-32601, `Method not found: ${call.method}`).catch(handleError)
        return
      }

      if (cleaners.asResult != null && call.id == null) {
        sendError(-32600, `Invalid JSON-RPC request: missing id`).catch(
          handleError
        )
        return
      }

      if (cleaners.asResult == null && call.id != null) {
        sendError(
          -32600,
          `Invalid JSON-RPC request: notification has an id`
        ).catch(handleError)
        return
      }

      let cleanParams
      try {
        cleanParams = cleaners.asParams(call.params)
      } catch (error) {
        sendError(-32602, `Invalid params: ${errorMessage(error)}`).catch(
          handleError
        )
        return
      }

      const out = localMethods[call.method](cleanParams)
      if (cleaners.asResult != null && _optionalChain([out, 'optionalAccess', _ => _.then]) != null) {
        const wasResult = uncleaner(cleaners.asResult)
        out.then(
          (result) => {
            const response = wasReturn({
              jsonrpc: '2.0',
              result: wasResult(result),
              error: undefined,
              id: _nullishCoalesce(call.id, () => ( null))
            })
            handleSend(JSON.stringify(response)).catch(handleError)
          },
          (error) => {
            sendError(1, errorMessage(error), call.id).catch(handleError)
          }
        )
      }
    } else if (response != null) {
      const { error, id, result } = response
      if (typeof id !== 'number') {
        // It's not a call we made...
        sendError(-32603, `Cannot find id ${String(id)}`, id).catch(handleError)
        return
      }
      const pendingCall = remoteCalls.get(id)

      // Handle subscription calls masquerading as returns:
      if (pendingCall == null) {
        const methodName = remoteSubscriptions.get(id)
        if (methodName == null) {
          sendError(-32603, `Cannot find id ${String(id)}`, id).catch(
            handleError
          )
          return
        }

        let cleanParams
        try {
          cleanParams = localCleaners[methodName].asParams(result)
        } catch (error) {
          sendError(-32602, `Invalid params: ${errorMessage(error)}`).catch(
            handleError
          )
          return
        }
        localMethods[methodName](cleanParams)
        return
      }
      remoteCalls.delete(id)

      if (error != null) {
        pendingCall.reject(new Error(error.message))
      } else {
        const { asResult } = pendingCall
        let cleanResult
        try {
          cleanResult = asResult(result)
        } catch (error) {
          pendingCall.reject(error)
        }
        pendingCall.resolve(cleanResult)
      }
    } else {
      sendError(-32600, `Invalid JSON-RPC request / response`).catch(
        handleError
      )
    }
  }

  function handleClose() {
    for (const call of remoteCalls.values()) {
      call.reject(new Error('JSON-RPC connection closed'))
    }
  }

  return {
    handleClose,
    handleMessage,
    remoteMethods: remoteMethods 
  }
}

function errorMessage(error) {
  return error instanceof Error ? error.message : String(error)
}























const asRpcId = (raw) => {
  if (raw === null) return raw
  if (typeof raw === 'string') return raw
  if (typeof raw === 'number' && Number.isInteger(raw)) return raw
  throw new TypeError('Expected a string or an integer')
}

export const asJsonRpcCall = asObject({
  jsonrpc: asValue('2.0'),
  method: asString,
  params: asUnknown,
  id: asOptional(asRpcId)
})

export const asJsonRpcReturn = asObject({
  jsonrpc: asValue('2.0'),
  result: asUnknown,
  error: asOptional(
    asObject({
      code: asNumber,
      message: asString,
      data: asUnknown
    })
  ),
  id: asRpcId
})
