import * as plugins from './plugins.js'; const protocol = plugins.servezoneInterfaces.protocol; /** * The oldest Cloudly release this client opens a `cloudlyClient` session with. * * It is raised only by the commit that starts depending on a later `@serve.zone/interfaces` * minor, and that raise is named in the changelog: a client that demands more than it needs * refuses controllers it could have served. What this client speaks is never stated here — it is * the installed release, which `createProtocolOffer` reads from the package itself. */ const cloudlyClientMinimumPeerVersion = '32.0.0'; /** This build's offer for the one session kind the api client opens. */ export const cloudlyClientProtocolOffer: plugins.servezoneInterfaces.protocol.IProtocolOffer = protocol.createProtocolOffer(cloudlyClientMinimumPeerVersion); /** * Two peers that cannot serve one session, named rather than collapsed into a transport failure. * * A refusal stands until an operator upgrades one of the two sides, so it is never retried: the * caller is rejected with it, and a refusal met while a reconnect restores its session — where no * caller waits and the transport publishes it only as the cause of its own denied restoration — * ends that socket lifecycle and is retained on the client instead of being offered again. */ export class CloudlyProtocolIncompatibleError extends Error { public readonly refusal: plugins.servezoneInterfaces.protocol.IProtocolRefusal; constructor( refusalArg: plugins.servezoneInterfaces.protocol.IProtocolRefusal, optionsArg?: ErrorOptions, ) { super(protocol.describeProtocolRefusal(refusalArg), optionsArg); this.name = 'CloudlyProtocolIncompatibleError'; this.refusal = refusalArg; } } /** * The payload a failed TypedRequest hop carried. * * Read structurally rather than through `instanceof TypedResponseError`: the request that fails * is created by TypedSocket, which resolves its own `@api.global/typedrequest`, so a class * identity check could miss a refusal in a consumer tree that holds a second copy. */ const readErrorData = (errorArg: unknown): unknown => { if (typeof errorArg !== 'object' || errorArg === null || !('errorData' in errorArg)) { return undefined; } return errorArg.errorData; }; /** * Rethrow a refused offer as this client's named error; every other failure passes untouched. * * The transport error stays attached as the cause, so the correlation a caller needs to find the * hop in a log is not lost behind the refusal. */ export const rethrowProtocolRefusal = (errorArg: unknown): never => { const refusal = protocol.readProtocolRefusal(readErrorData(errorArg)); if (refusal === null) { throw errorArg; } throw new CloudlyProtocolIncompatibleError(refusal, { cause: errorArg }); }; /** * The controller's own offer, judged from this client's side. * * Acceptance is the controller's decision about this client; whether this client can serve the * session it was just given is this check, and a client that cannot must not use it. */ export const requireCompatibleProtocol = ( peerOfferArg: plugins.servezoneInterfaces.protocol.IProtocolOffer, ): void => { const negotiation = protocol.negotiateProtocol(cloudlyClientProtocolOffer, peerOfferArg); if (!negotiation.compatible) { throw new CloudlyProtocolIncompatibleError(negotiation.refusal); } };