// Copyright 2026 Synnax Labs, Inc. // // Use of this software is governed by the Business Source License included in the file // licenses/BSL.txt. // // As of the Change Date specified in that file, in accordance with the Business Source // License, use of this software will be governed by the Apache License, Version 2.0, // included in the file licenses/APL.txt. import { breaker, errors } from "@synnaxlabs/x"; import { type z } from "zod"; import { Unreachable } from "@/errors"; import { type Middleware } from "@/middleware"; import { type Transport } from "@/transport"; /** * An interface for an entity that implements a simple request-response transport * between two entities. */ export interface UnaryClient extends Transport { /** * Sends a request to the target server and waits until a response is received. * @param resSchema - The schema to validate the response against. * @returns the decoded response. * @throws Error: if the server returns an error or the transport fails. */ send: ( target: string, req: z.input | z.infer, reqSchema: RQ, resSchema: RS, ) => Promise>; } export const unaryWithBreaker = ( base: UnaryClient, cfg: breaker.Config, ): UnaryClient => { class WithBreaker implements UnaryClient { readonly wrapped: UnaryClient; constructor(wrapper: UnaryClient) { this.wrapped = wrapper; } use(...mw: Middleware[]) { this.wrapped.use(...mw); } async send( target: string, req: z.input | z.infer, reqSchema: RQ, resSchema: RS, ): Promise> { const brk = new breaker.Breaker(cfg); do try { return await this.wrapped.send(target, req, reqSchema, resSchema); } catch (err) { const e = errors.fromUnknown(err); if (!Unreachable.matches(e) || !brk.canRetry) throw e; console.warn(`[freighter] ${brk.retryMessage}`, e); await brk.wait(); } while (true); } } return new WithBreaker(base); };