146 lines · 4.0 KB · TypeScript
RawDownload
// @effect-diagnostics globalTimers:off -- This protocol broker owns cancellable request deadlines outside the Effect runtime.import {  DESKTOP_APP_ACTIVATION_PROTOCOL_VERSION,  type DesktopAppActivationFailure,  type DesktopAppActivationRequest,  type DesktopAppActivationResponse,} from "@t3tools/contracts"; interface PendingActivation {  readonly request: DesktopAppActivationRequest;  readonly resolve: (response: DesktopAppActivationResponse) => void;  readonly timeout: ReturnType<typeof setTimeout>;  dispatched: boolean;} type RendererSender = (request: DesktopAppActivationRequest) => void; function failure(  requestId: string,  code: DesktopAppActivationFailure["code"],  message: string,): DesktopAppActivationFailure {  return {    version: DESKTOP_APP_ACTIVATION_PROTOCOL_VERSION,    requestId,    ok: false,    code,    message,  };} /** Holds CLI requests until the real desktop renderer is ready to handle them. */export class DesktopAppActivationBroker {  readonly #pending = new Map<string, PendingActivation>();  readonly #requestTimeoutMs: number;  readonly #activate: () => void;  #renderer: RendererSender | null = null;  #closed = false;   constructor(input: { readonly requestTimeoutMs: number; readonly activate: () => void }) {    this.#requestTimeoutMs = input.requestTimeoutMs;    this.#activate = input.activate;  }   request(request: DesktopAppActivationRequest): Promise<DesktopAppActivationResponse> {    if (this.#closed) {      return Promise.resolve(        failure(request.requestId, "renderer-unavailable", "T3 Code is shutting down."),      );    }    if (this.#pending.has(request.requestId)) {      return Promise.resolve(        failure(request.requestId, "invalid-request", "The request id is already in use."),      );    }     const response = new Promise<DesktopAppActivationResponse>((resolve) => {      const timeout = setTimeout(() => {        this.#settle(          failure(            request.requestId,            "request-timeout",            "The desktop app did not finish opening the project in time.",          ),        );      }, this.#requestTimeoutMs);      this.#pending.set(request.requestId, {        request,        resolve,        timeout,        dispatched: false,      });    });     this.#activate();    this.#flush();    return response;  }   registerRenderer(send: RendererSender): void {    this.#renderer = send;    this.#flush();  }   clearRenderer(): void {    this.#renderer = null;    for (const pending of this.#pending.values()) {      if (pending.dispatched) {        this.#settle(          failure(            pending.request.requestId,            "renderer-unavailable",            "The T3 Code window closed before it opened the project.",          ),        );      }    }  }   complete(response: DesktopAppActivationResponse): void {    this.#settle(response);  }   cancel(requestId: string): void {    this.#settle(      failure(requestId, "renderer-unavailable", "The command closed before T3 Code was ready."),    );  }   close(): void {    this.#closed = true;    this.#renderer = null;    for (const pending of this.#pending.values()) {      this.#settle(        failure(pending.request.requestId, "renderer-unavailable", "T3 Code is shutting down."),      );    }  }   #flush(): void {    const renderer = this.#renderer;    if (renderer === null) return;    if ([...this.#pending.values()].some((pending) => pending.dispatched)) return;     for (const pending of this.#pending.values()) {      if (pending.dispatched) continue;      try {        pending.dispatched = true;        renderer(pending.request);      } catch {        pending.dispatched = false;        this.#renderer = null;      }      return;    }  }   #settle(response: DesktopAppActivationResponse): void {    const pending = this.#pending.get(response.requestId);    if (!pending) return;    clearTimeout(pending.timeout);    this.#pending.delete(response.requestId);    pending.resolve(response);    this.#flush();  }}