// Copyright 2024, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 import { handleWaveEvent } from "@/app/store/wps"; import * as util from "@/util/util"; import debug from "debug"; const dlog = debug("wave:router"); const SysRouteName = "sys"; type RouteInfo = { rpcId: string; sourceRouteId: string; destRouteId: string; }; function makeFeBlockRouteId(feBlockId: string): string { return `feblock:${feBlockId}`; } function makeTabRouteId(tabId: string): string { return `tab:${tabId}`; } class WshRouter { routeMap: Map; // routeid -> client upstreamClient: AbstractWshClient; rpcMap: Map; // rpcid -> routeinfo constructor(upstreamClient: AbstractWshClient) { this.routeMap = new Map(); this.rpcMap = new Map(); if (upstreamClient == null) { throw new Error("upstream client cannot be null"); } this.upstreamClient = upstreamClient; } reannounceRoutes() { for (const [routeId, client] of this.routeMap) { const announceMsg: RpcMessage = { command: "routeannounce", data: routeId, source: routeId, }; this.upstreamClient.recvRpcMessage(announceMsg); } } // returns true if the message was sent _sendRoutedMessage(msg: RpcMessage, destRouteId: string) { const client = this.routeMap.get(destRouteId); if (client) { client.recvRpcMessage(msg); return; } // there should always an upstream client if (!this.upstreamClient) { throw new Error(`no upstream client for message: ${msg}`); } this.upstreamClient?.recvRpcMessage(msg); } _registerRouteInfo(reqid: string, sourceRouteId: string, destRouteId: string) { dlog("registering route info", reqid, sourceRouteId, destRouteId); if (util.isBlank(reqid)) { return; } const routeInfo: RouteInfo = { rpcId: reqid, sourceRouteId: sourceRouteId, destRouteId: destRouteId, }; this.rpcMap.set(reqid, routeInfo); } recvRpcMessage(msg: RpcMessage) { dlog("router received message", msg); // we are a terminal node by definition, so we don't need to process with announce/unannounce messages if (msg.command == "routeannounce" || msg.command == "routeunannounce") { return; } // handle events if (msg.command == "eventrecv") { handleWaveEvent(msg.data); return; } if (!util.isBlank(msg.command)) { // send + register routeinfo if (!util.isBlank(msg.reqid)) { this._registerRouteInfo(msg.reqid, msg.source, msg.route); } this._sendRoutedMessage(msg, msg.route); return; } if (!util.isBlank(msg.reqid)) { const routeInfo = this.rpcMap.get(msg.reqid); if (!routeInfo) { // no route info, discard dlog("no route info for reqid, discarding", msg); return; } this._sendRoutedMessage(msg, routeInfo.destRouteId); return; } if (!util.isBlank(msg.resid)) { const routeInfo = this.rpcMap.get(msg.resid); if (!routeInfo) { // no route info, discard dlog("no route info for resid, discarding", msg); return; } this._sendRoutedMessage(msg, routeInfo.sourceRouteId); if (!msg.cont) { dlog("deleting route info", msg.resid); this.rpcMap.delete(msg.resid); } return; } dlog("bad rpc message recevied by router, no command, reqid, or resid (discarding)", msg); } registerRoute(routeId: string, client: AbstractWshClient) { if (routeId == SysRouteName) { throw new Error(`Cannot register route with reserved name (${routeId})`); } dlog("registering route: ", routeId); // announce const announceMsg: RpcMessage = { command: "routeannounce", data: routeId, source: routeId, }; this.upstreamClient.recvRpcMessage(announceMsg); this.routeMap.set(routeId, client); } unregisterRoute(routeId: string) { dlog("unregister route: ", routeId); // unannounce const unannounceMsg: RpcMessage = { command: "routeunannounce", data: routeId, source: routeId, }; this.upstreamClient?.recvRpcMessage(unannounceMsg); this.routeMap.delete(routeId); } } export { makeFeBlockRouteId, makeTabRouteId, WshRouter };