• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

akoidan / hotkey-hub / 22072236661

16 Feb 2026 05:28PM UTC coverage: 65.192%. First build
22072236661

Pull #45

github

akoidan
ddfasd
Pull Request #45: Develop

141 of 351 branches covered (40.17%)

Branch coverage included in aggregate %.

201 of 364 new or added lines in 59 files covered. (55.22%)

1215 of 1729 relevant lines covered (70.27%)

23.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

7.2
/src/client/http-client.ts
1
import {Inject, Injectable, Logger} from '@nestjs/common';
2✔
2
import {Agent, request} from 'https';
2✔
3
import {ConfigService} from '@/config/config-service';
2✔
4
import clc from 'cli-color';
2✔
5
import {SemaphorService} from '@/semaphor/semaphor-service';
2✔
6
import {ASYNC_PROVIDER} from '@/asyncstore/async-storage-const';
2✔
7
import {AsyncLocalStorage} from 'async_hooks';
2✔
8
import {CustomError, RequestOptions, TIMEOUT} from '@/client/client-model';
2✔
9

10

11
@Injectable()
12
export class FetchClient {
2✔
13
  constructor(
14
    private readonly logger: Logger,
×
15
    private readonly config: ConfigService,
×
16
    private readonly agent: Agent,
×
17
    private readonly semaphorService: SemaphorService,
×
18
    @Inject(TIMEOUT)
NEW
19
    private readonly timeout: number,
×
20
    @Inject(ASYNC_PROVIDER)
NEW
21
    private readonly asyncLocalStorage: AsyncLocalStorage<Map<string, any>>,
×
22
  ) {
23
  }
24

25
  // eslint-disable-next-line max-lines-per-function
26
  private async executeRequest(
27
    method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
28
    client: string,
29
    url: string,
30
    payloadstr: string | null,
31
    controller: AbortController
32
  ): Promise<[string, number]> {
33
    const ips = this.config.getIps();
×
NEW
34
    let host = ips[client];
×
35
    let port: number;
NEW
36
    if (host.includes(':')) {
×
NEW
37
      [host] = host.split(':');
×
NEW
38
      port = parseInt(host.split(':')[1], 10);
×
39
    } else {
NEW
40
      port = this.config.getClientPort();
×
41
    }
42
    if (!host) {
×
43
      const error = Error();
×
44
      (error as CustomError).statusCode = 1;
×
45
      (error as CustomError).response = `Desination "${client}" doesn't exist. Available are ${JSON.stringify(ips)}`;
×
46
      throw error;
×
47
    }
48
    return new Promise<[string, number]>((resolve, reject) => {
×
49
      const headers = this.getHeaders(payloadstr);
×
50
      const req = request({
×
51
        agent: this.agent,
52
        port,
53
        host,
54
        signal: controller.signal,
55
        protocol: 'https:',
56
        path: url,
57
        method,
58
        headers,
59
      }, (res) => {
60
        let data = '';
×
61
        res.on('data', (chunk: string) => (data += chunk));
×
62
        res.on('end', () => {
×
63
          if (res.statusCode! < 400) {
×
64
            resolve([data, res.statusCode!]);
×
65
          } else {
66
            const error = Error();
×
67
            (error as CustomError).statusCode = res.statusCode;
×
68
            (error as CustomError).response = data;
×
69
            reject(error);
×
70
          }
71
        });
72
        res.on('error', (error: Error) => reject(error));
×
73
      });
74

NEW
75
      if (payloadstr) {
×
76
        req.write(payloadstr);
×
77
      }
NEW
78
      this.logger.debug(`Executing ${method} https://${host}:${port}${url} ${payloadstr ?? ''}`);
×
79
      req.end();
×
80
    });
81
  }
82

83
  private getHeaders(payloadstr: string | null): Record<string, string | number> {
84
    let headers: Record<string, string | number> = {
×
85
      // eslint-disable-next-line @typescript-eslint/naming-convention
86
      'x-request-id': this.semaphorService.getCurrentOperationId(),
87
    };
88
    if (payloadstr) {
×
89
      headers = {
×
90
        ...headers,
91
        // eslint-disable-next-line @typescript-eslint/naming-convention
92
        'Content-Type': 'application/json',
93
        // eslint-disable-next-line @typescript-eslint/naming-convention
94
        'Content-Length': Buffer.byteLength(payloadstr),
95
      };
96
    }
97
    return headers;
×
98
  }
99

100
  // eslint-disable-next-line max-lines-per-function
101
  private async makeRequest<T>(
102
    method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH',
103
    client: string,
104
    url: string,
105
    options: RequestOptions = {},
×
106
  ): Promise<T> {
NEW
107
    const payloadstr: string | null = options.payload ? JSON.stringify(options.payload) : null;
×
NEW
108
    if (options.query) {
×
NEW
109
      url += `?${new URLSearchParams(options.query).toString()}`;
×
110
    }
111
    try {
×
NEW
112
      const httpController = new AbortController(); // otherwise it will fail all commands
×
NEW
113
      let timeout: NodeJS.Timeout | null = null;
×
NEW
114
      let reject: ((error: Error) => void) |null = null ;
×
NEW
115
      const controller = this.asyncLocalStorage.getStore()?.get(SemaphorService.ABORT_CONTROLLER) as AbortController;
×
NEW
116
      const combKey  = this.asyncLocalStorage.getStore()!.get(SemaphorService.COMB_KEY) as string;
×
NEW
117
      const eventListener = (): void =>  {
×
118
        // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
NEW
119
        this.logger.debug(`Aborting request ${combKey}, and clearing timeout ${timeout}`);
×
NEW
120
        clearTimeout(timeout!);
×
NEW
121
        httpController.abort();
×
NEW
122
        reject!(Error(controller.signal.reason as string));
×
123
      };
124
      const [result, statusCode] = await Promise.race([
×
125
        this.executeRequest(method, client, url, payloadstr, httpController),
126
        new Promise<never>((_, innerReject) => {
NEW
127
          reject = innerReject;
×
NEW
128
          timeout = setTimeout(() => {
×
NEW
129
            this.logger.debug(`Request timed out after ${this.timeout}ms`);
×
NEW
130
            controller.signal.removeEventListener('abort', eventListener);
×
NEW
131
            httpController.abort();
×
NEW
132
            innerReject(Error(`Request timed out after ${this.timeout}ms`));
×
133
          }, options.timeout ?? this.timeout);
×
134
          // eslint-disable-next-line
NEW
135
          this.logger.verbose(`Added timeout #${timeout} for ${options.timeout ?? this.timeout}ms`);
×
NEW
136
          controller.signal.addEventListener('abort', eventListener);
×
137
        }),
138
      ]);
NEW
139
      controller.signal.removeEventListener('abort', eventListener);
×
NEW
140
      clearTimeout(timeout!);
×
141

142
      this.logger.log(
×
143
        `${method}:${statusCode} ${clc.bold.green(client)} ${clc.yellow(url)} ` +
144
        `${payloadstr ?? ''} ${clc.xterm(7)('==>>')} ${result || 'void'}`
×
145
      );
NEW
146
      if ((statusCode as unknown as number) !== 204 && result) {
×
147
        try {
×
148
          return JSON.parse(result) as T;
×
149
        } catch (error) {
150
          throw new Error(`Failed to parse ${result}`);
×
151
        }
152
      }
153
      return null as T;
×
154
    } catch (error: unknown) {
155
      const status: number | 'FAIL' = (error as CustomError).statusCode ?? 'FAIL';
×
NEW
156
      let hostname = this.config.getIps()[client];
×
NEW
157
      if (!hostname.includes(':')) {
×
NEW
158
        hostname = `${hostname}:${this.config.getClientPort()}`;
×
159
      }
NEW
160
      const fullUrl: string = `https://${hostname}${url}`;
×
161
      throw new Error(
×
162
        `${method}:${client}:${status} ${fullUrl} ${(error as Error).message}`
163
        + ` ${payloadstr ?? ''} ${clc.xterm(2)('==>>')} ${(error as CustomError).response ?? 'void'}`
×
164
      );
165
    }
166
  }
167

168
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
169
  async post<T>(client: string, url: string, options: RequestOptions = {}): Promise<T> {
×
NEW
170
    return this.makeRequest<T>('POST', client, url, options);
×
171
  }
172

173
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
174
  async patch<T>(client: string, url: string, options: RequestOptions = {}): Promise<T> {
×
NEW
175
    return this.makeRequest<T>('PATCH', client, url, options);
×
176
  }
177

178
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
179
  async put<T>(client: string, url: string, options: RequestOptions = {}): Promise<T> {
×
NEW
180
    return this.makeRequest<T>('PUT', client, url, options);
×
181
  }
182

183
  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
184
  async delete<T>(client: string, url: string, options: RequestOptions = {}): Promise<T> {
×
NEW
185
    return this.makeRequest<T>('DELETE', client, url, options);
×
186
  }
187

188
  async get<T>(client: string, url: string, options: RequestOptions = {}): Promise<T> {
×
NEW
189
    return this.makeRequest<T>('GET', client, url, options);
×
190
  }
191
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc