// Copyright 2021 The Pigweed Authors // // Licensed 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 // // https://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. import {Message} from 'google-protobuf'; import {Status} from '@pigweed/pw_status'; import {Call} from './call'; import {Channel, Method, Service} from './descriptors'; import * as packets from './packets'; /** Data class for a pending RPC call. */ export class Rpc { readonly channel: Channel; readonly service: Service; readonly method: Method; constructor(channel: Channel, service: Service, method: Method) { this.channel = channel; this.service = service; this.method = method; } /** Returns channel service method id tuple */ get idSet(): [number, number, number] { return [this.channel.id, this.service.id, this.method.id]; } /** * Returns a string sequence to uniquely identify channel, service, and * method. This can be used to hash the Rpc. * * For example: "12346789.23452345.12341234" */ get idString(): string { return `${this.channel.id}.${this.service.id}.${this.method.id}`; } toString(): string { return ( `${this.service.name}.${this.method.name} on channel ` + `${this.channel.id}` ); } } /** Tracks pending RPCs and encodes outgoing RPC packets. */ export class PendingCalls { pending: Map = new Map(); /** Starts the provided RPC and returns the encoded packet to send. */ request(rpc: Rpc, request: Message, call: Call): Uint8Array { this.open(rpc, call); console.log(`Starting ${rpc}`); return packets.encodeRequest(rpc.idSet, request); } /** Calls request and sends the resulting packet to the channel. */ sendRequest( rpc: Rpc, call: Call, ignoreError: boolean, request?: Message ): Call | undefined { const previous = this.open(rpc, call); const packet = packets.encodeRequest(rpc.idSet, request); try { rpc.channel.send(packet); } catch (error) { if (!ignoreError) { throw error; } } return previous; } /** * Creates a call for an RPC, but does not invoke it. * * open() can be used to receive streaming responses to an RPC that was not * invoked by this client. For example, a server may stream logs with a * server streaming RPC prior to any clients invoking it. */ open(rpc: Rpc, call: Call): Call | undefined { console.debug(`Starting ${rpc}`); const previous = this.pending.get(rpc.idString); this.pending.set(rpc.idString, call); return previous; } sendClientStream(rpc: Rpc, message: Message) { if (this.getPending(rpc) === undefined) { throw new Error(`Attempt to send client stream for inactive RPC: ${rpc}`); } rpc.channel.send(packets.encodeClientStream(rpc.idSet, message)); } sendClientStreamEnd(rpc: Rpc) { if (this.getPending(rpc) === undefined) { throw new Error(`Attempt to send client stream for inactive RPC: ${rpc}`); } rpc.channel.send(packets.encodeClientStreamEnd(rpc.idSet)); } /** Cancels the RPC. Returns the CANCEL packet to send. */ cancel(rpc: Rpc): Uint8Array | undefined { console.debug(`Cancelling ${rpc}`); this.pending.delete(rpc.idString); if (rpc.method.clientStreaming && rpc.method.serverStreaming) { return undefined; } return packets.encodeCancel(rpc.idSet); } /** Calls cancel and sends the cancel packet, if any, to the channel. */ sendCancel(rpc: Rpc): boolean { let packet: Uint8Array | undefined; try { packet = this.cancel(rpc); } catch (err) { return false; } if (packet !== undefined) { rpc.channel.send(packet); } return true; } /** Gets the pending RPC's call. If status is set, clears the RPC. */ getPending(rpc: Rpc, status?: Status): Call | undefined { if (status === undefined) { return this.pending.get(rpc.idString); } const call = this.pending.get(rpc.idString); this.pending.delete(rpc.idString); return call; } }