/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Agent } from "#endpoint/Agent.js"; import { Node } from "#node/Node.js"; import { ServerNode } from "#node/ServerNode.js"; import { Abort, asError, Diagnostic, InternalError, Logger } from "@matter/general"; import { Mark } from "@matter/protocol"; import { StatusResponse, StatusResponseError } from "@matter/types"; import { ApiPath } from "./ApiPath.js"; import { ApiResource } from "./ApiResource.js"; import { LocalResponse } from "./LocalResponse.js"; import { RemoteRequest } from "./RemoteRequest.js"; import { RemoteResponse } from "./RemoteResponse.js"; import { EndpointResource } from "./resources/EndpointResource.js"; import { NodeResource } from "./resources/NodeResource.js"; import { ServerNodeResource } from "./resources/ServerNodeResource.js"; const loggers = new Map(); /** * Common substrate for the network APIs. * * The logical API covers RPC and read/write semantics. These involve a "/"-separated path and a logical operation * (read, write, invoke, etc.) * * This namespace provides utilities for mapping paths to resources and taking action on the resource. */ export namespace Api { /** * Retrieve the {@link ApiResource} for a path. */ export async function resourceFor(agent: Agent, path: ApiPath): Promise { let item; if ("peers" in agent.endpoint) { item = new ServerNodeResource(agent, undefined); } else if (agent.endpoint instanceof Node) { item = new NodeResource(agent, undefined); } else { item = new EndpointResource(agent, undefined); } const breadcrumb: ApiResource[] = [item]; for (const segment of path) { const parent = breadcrumb[breadcrumb.length - 1]; const item = await parent.childFor(segment); if (!item) { return; } breadcrumb.push(item); } return breadcrumb[breadcrumb.length - 1]; } export function log(level: "error" | "info", facility: string, id: string | undefined, ...message: unknown[]) { let logger = loggers.get(facility); if (!logger) { loggers.set(facility, (logger = Logger.get(facility))); } logger[level](Diagnostic.via(id || "(anon)"), message); } export function logRequest(facility: string, id: string | undefined, method: string, target: string) { log("info", facility, id, Mark.INBOUND, Diagnostic.strong(method), target); } export function logResponse(facility: string, response: RemoteResponse) { const message = Array(Mark.OUTBOUND, RemoteResponse.describe(response)); let level: "error" | "info"; switch (response.kind) { case "error": message.push(Diagnostic.errorMessage({ id: response.code, message: response.message })); level = "error"; break; default: level = "info"; break; } log(level, facility, response.id, message); } /** * Execute a {@link RemoteRequest}. */ export async function execute( facility: string, node: ServerNode, request: RemoteRequest, signal: Abort.Signal, ): Promise { const { target, method, id } = request; logRequest(facility, id, method, target); try { const message = await node.act("remote", async (agent): Promise => { const item = await resourceFor(agent, new ApiPath(target)); if (item === undefined) { throw new StatusResponse.NotFoundError(`Target "${target}" not found`); } switch (method) { case "read": const value = item.read(); if (value === undefined) { throw new StatusResponse.UnsupportedReadError(`Target "${target}" is not readable`); } value.convertToJson(); return { kind: "value", id, // TODO - consider handling serialization here (in agent context) so copy is unnecessary value, }; case "write": item.write({ js: request.value }); return { kind: "ok", id }; case "add": item.add({ js: request.value }); return { kind: "ok", id }; case "delete": item.delete(); return { kind: "ok", id }; case "invoke": { const value = await item.invoke({ js: request.parameters }); if (value?.js === undefined || value?.js === null) { return { kind: "ok", id }; } value.convertToJson(); return { id, kind: "value", value }; } case "subscribe": { const options = { ...request } as Record; for (const field of ["target", "id", "method"]) { delete options[field]; } const stream = item.subscribe(signal, { id, js: options, }); return { kind: "subscription", id, stream }; } } }); return message; } catch (error) { return errorResponseOf(facility, id, error); } } export function errorResponseOf(facility: string, id: string | undefined, error: unknown): LocalResponse { error = asError(error); // User-facing message const sre = StatusResponseError.of(error); if (sre) { return { kind: "error", id, error: sre }; } // Internal error log("error", facility, id, "Internal error:", error); return { kind: "error", id, error: new InternalError() }; } }