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

IGVF-DACC / igvf-ui / #5473

16 Apr 2025 09:58PM UTC coverage: 99.952%. Remained the same
#5473

push

forresttanaka
IGVF-2161-file-preview-update

Also try 4MB max read length.

1281 of 1282 branches covered (99.92%)

Branch coverage included in aggregate %.

1 of 1 new or added line in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

2904 of 2905 relevant lines covered (99.97%)

17.46 hits per line

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

98.98
/lib/fetch-request.ts
1
/**
2
 * Use the FetchRequest class to send requests to a server, whether the NextJS server or the data
3
 * provider, for example GET or POST requests.
4
 *
5
 * const request = new FetchRequest();
6
 * const response = await request.getObject('/api/users/1');
7
 * *
8
 * Generally, the last part of the method names reflects the type of data resolved by the returned
9
 * promise, e.g. getObject() resolves to an object, while getText() resolves to a string. Methods
10
 * relying on these fundamental methods might not follow this naming convention.
11
 *
12
 * Handle authentication when calling the FetchRequest constructor. For requests from the server
13
 * code, pass the browser cookie in the `cookie` property:
14
 *
15
 * const request = new FetchRequest({ cookie: req.headers.cookie });
16
 *
17
 * For requests from the client code, pass the session object in the `session` property:
18
 *
19
 * const request = new FetchRequest({ session });
20
 *
21
 * You can also pass nothing to the constructor for requests not requiring authentication.
22
 *
23
 * Many methods accept an optional `defaultErrorValue` parameter. If the request fails, this value
24
 * gets returned. If you instead want requests that fail to return an error object, don't pass
25
 * `defaultErrorValue`.
26
 */
27

28
// node_modules
29
import pako from "pako";
30✔
30
// lib
31
import { API_URL, SERVER_URL, BACKEND_URL, MAX_URL_LENGTH } from "./constants";
30✔
32

33
// TYPES
34
// root
35
import type {
36
  DatabaseObject,
37
  DataProviderObject,
38
  SessionObject,
39
} from "../globals.d";
40
// lib
41
import type {
42
  ErrorObject,
43
  FetchMethod,
44
  FetchRequestInitializer,
45
} from "./fetch-request.d";
46
import { ok, err, Result, Ok } from "./result";
30✔
47

48
export const FETCH_METHOD = {
30✔
49
  GET: "GET",
50
  HEAD: "HEAD",
51
  POST: "POST",
52
  PUT: "PUT",
53
  DELETE: "DELETE",
54
  CONNECT: "CONNECT",
55
  OPTIONS: "OPTIONS",
56
  TRACE: "TRACE",
57
  PATCH: "PATCH",
58
};
59
Object.freeze(FETCH_METHOD);
30✔
60

61
/**
62
 * fetch() methods that allow a `body` in the options object.
63
 */
64
const METHODS_ALLOWING_BODY: Array<FetchMethod> = ["POST", "PUT", "PATCH"];
30✔
65
Object.freeze(METHODS_ALLOWING_BODY);
30✔
66

67
const PAYLOAD_FORMAT: { [key: string]: string } = {
30✔
68
  JSON: "application/json",
69
  HTML: "text/html",
70
  TEXT: "text/plain",
71
  XML: "application/xml",
72
  FORM: "application/x-www-form-urlencoded",
73
  FORM_DATA: "multipart/form-data",
74
  JSON_PATCH: "application/json-patch+json",
75
  JSON_MERGE_PATCH: "application/json-merge-patch+json",
76
  JSON_PATCH_JSON: "application/json-patch+json",
77
  JSON_MERGE_PATCH_JSON: "application/json-merge-patch+json",
78
};
79
Object.freeze(PAYLOAD_FORMAT);
30✔
80

81
export const HTTP_STATUS_CODE: { [key: string]: number } = {
30✔
82
  OK: 200,
83
  CREATED: 201,
84
  ACCEPTED: 202,
85
  NO_CONTENT: 204,
86
  PARTIAL_CONTENT: 206,
87
  MULTIPLE_CHOICES: 300,
88
  MOVED_PERMANENTLY: 301,
89
  FOUND: 302,
90
  SEE_OTHER: 303,
91
  NOT_MODIFIED: 304,
92
  USE_PROXY: 305,
93
  SWITCH_PROXY: 306,
94
  TEMPORARY_REDIRECT: 307,
95
  BAD_REQUEST: 400,
96
  UNAUTHORIZED: 401,
97
  PAYMENT_REQUIRED: 402,
98
  FORBIDDEN: 403,
99
  NOT_FOUND: 404,
100
  METHOD_NOT_ALLOWED: 405,
101
  NOT_ACCEPTABLE: 406,
102
  PROXY_AUTHENTICATION_REQUIRED: 407,
103
  REQUEST_TIMEOUT: 408,
104
  CONFLICT: 409,
105
  GONE: 410,
106
  LENGTH_REQUIRED: 411,
107
  PRECONDITION_FAILED: 412,
108
  REQUEST_ENTITY_TOO_LARGE: 413,
109
  REQUEST_URI_TOO_LONG: 414,
110
  UNSUPPORTED_MEDIA_TYPE: 415,
111
  REQUESTED_RANGE_NOT_SATISFIABLE: 416,
112
  EXPECTATION_FAILED: 417,
113
  IM_A_TEAPOT: 418,
114
  UNPROCESSABLE_ENTITY: 422,
115
  TOO_MANY_REQUESTS: 429,
116
  INTERNAL_SERVER_ERROR: 500,
117
  NOT_IMPLEMENTED: 501,
118
  BAD_GATEWAY: 502,
119
  SERVICE_UNAVAILABLE: 503,
120
  GATEWAY_TIMEOUT: 504,
121
  HTTP_VERSION_NOT_SUPPORTED: 505,
122
};
123
Object.freeze(HTTP_STATUS_CODE);
30✔
124

125
/**
126
 * Standard returned response for a network error.
127
 */
128
const NETWORK_ERROR_RESPONSE: ErrorObject = {
30✔
129
  isError: true,
130
  "@type": ["NetworkError", "Error"],
131
  status: "error",
132
  code: HTTP_STATUS_CODE.SERVICE_UNAVAILABLE,
133
  title: "Unknown error",
134
  description: "An unknown error occurred.",
135
  detail: "An unknown error occurred.",
136
};
137
Object.freeze(NETWORK_ERROR_RESPONSE);
30✔
138

139
/**
140
 * Estimate of the maximum size of an @id=path query-string element.
141
 */
142
const MAX_PATH_QUERY_LENGTH_ESTIMATE = 50;
30✔
143

144
/**
145
 * Maximum number of bytes to read from a gzipped text file. This must have a value enough for
146
 * successful decompression.
147
 */
148
const MAX_READ_SIZE = 4_000_000;
30✔
149

150
/**
151
 * Default maximum number of lines to return from the text file preview methods. Make sure this has
152
 * a value less than `MAX_READ_LINES`.
153
 */
154
const DEFAULT_MAX_TEXT_LINES = 100;
30✔
155

156
/**
157
 * Log a request from the NextJS server to igvfd.
158
 * @param {string} method FetchRequest method that performs the request
159
 * @param {string} path Path or paths to requested resource
160
 * @returns {void}
161
 */
162
function logRequest(method: string, path: string): void {
163
  const date = new Date().toISOString();
136✔
164
  console.log(`SVRREQ [${date}] ${method} ${path}`);
136✔
165
}
166

167
/**
168
 * Type guard to check if `item` is an `ErrorObject`. Pass the result of the `union()` method to
169
 * this function so items that are `ErrorObject` type are automatically treated as that type.
170
 * @param item Item to check whether it's an ErrorObject or actual data
171
 * @returns True if `item` is an `ErrorObject`
172
 */
173
export function isErrorObject(
2✔
174
  item: DataProviderObject | ErrorObject
175
): item is ErrorObject {
176
  return (item as ErrorObject).isError === true;
2✔
177
}
178

179
/**
180
 * Make requests to the server or data provider.
181
 * @param {object} {
182
 *   cookie: Cookie from NextJS request object; used to authenticate server-side requests
183
 *   session: Session object from the data server for authenticating client-side requests
184
 * }
185
 */
186
export default class FetchRequest {
127✔
187
  private headers = new Headers();
122✔
188
  private backend = false;
122✔
189

190
  /**
191
   * Determine whether the response object indicates an error of any kind occurred, whether an
192
   * error detected by the server, or a network error. Objects without an `@type` property return
193
   * true (success).
194
   * @param {DataProviderObject|ErrorObject} response Response object from fetch()
195
   * @returns {boolean} True if response is a successful response
196
   */
197
  static isResponseSuccess(
198
    response: DataProviderObject | ErrorObject
199
  ): response is DataProviderObject {
200
    if (
5✔
201
      typeof response === "object" &&
15✔
202
      response !== null &&
203
      "@type" in response
204
    ) {
205
      const types = (response as DatabaseObject)["@type"];
4✔
206
      return !types.includes("Error");
4✔
207
    }
208
    return true;
1✔
209
  }
210

211
  constructor(authentication?: FetchRequestInitializer) {
212
    let cookie: string | undefined;
213
    let session: SessionObject | undefined;
214
    let backend: boolean | undefined;
215

216
    if (authentication) {
122✔
217
      ({ cookie, session, backend } = authentication);
65✔
218
      if (cookie && session) {
65✔
219
        throw new Error(
1✔
220
          "Must authenticate with either cookie (server-side requests) or session (client-side requests) but not both"
221
        );
222
      }
223
      if (!backend) {
64✔
224
        if (this.isServer && session) {
18✔
225
          throw new Error(
1✔
226
            "Server-side requests requires a cookie, but a session was provided."
227
          );
228
        }
229
        if (!this.isServer && cookie) {
17✔
230
          throw new Error(
1✔
231
            "Client-side requests requires a session, but a cookie was provided."
232
          );
233
        }
234
      }
235
    }
236

237
    // Initialize the HTTP request headers.
238
    if (cookie && this.isServer) {
119✔
239
      this.headers.append("Cookie", cookie);
3✔
240
    }
241
    if (session && !this.isServer && !backend) {
119✔
242
      this.headers.append("X-CSRF-Token", session._csrft_);
11✔
243
    }
244

245
    if (backend) {
119✔
246
      this.backend = true;
46✔
247
    }
248
  }
249

250
  /**
251
   * Take an array of paths to database objects, and break it into groups of paths to fit within
252
   * the maximum size of a query string -- each group an array of paths with a maximum calculated
253
   * size. This function returns an array of these groups -- an array of arrays of paths, with no
254
   * sub-array having a length greater than the amount that would fit within a URL.
255
   * @param {Array<string>} paths Path of each object to request
256
   * @param {number} adjustment Number of characters to subtract from the URL length for other
257
   *     query-string elements
258
   * @returns {Array<Array<string>>} Array of arrays (groups) of paths
259
   */
260
  private pathsIntoPathGroups(
261
    paths: Array<string>,
262
    adjustment: number
263
  ): Array<Array<string>> {
264
    // Calculate the maximum number of paths that can fit into a query string.
265
    const maxGroupSize = Math.floor(
33✔
266
      (MAX_URL_LENGTH - adjustment) / MAX_PATH_QUERY_LENGTH_ESTIMATE
267
    );
268

269
    // Break the paths into groups of maxGroupSize. Each group gets converted to a query string
270
    // and sent as a single request.
271
    const pathGroups = paths.reduce(
33✔
272
      (groups: Array<Array<string>>, path: string) => {
273
        const lastGroup = groups[groups.length - 1];
154✔
274
        if (lastGroup.length < maxGroupSize) {
154✔
275
          // The last group in the array of groups still has room for a new path.
276
          lastGroup.push(path);
153✔
277
          return groups;
153✔
278
        }
279

280
        // No room for another path in the last group. Make a new last group at the end.
281
        return [...groups, [path]];
1✔
282
      },
283
      [[]]
284
    );
285
    return pathGroups;
33✔
286
  }
287

288
  /**
289
   * Determine whether this class object is being used on the server or not.
290
   * @returns {boolean} True if this class object exists on server, false for client
291
   */
292
  private get isServer(): boolean {
293
    return typeof window === "undefined";
175✔
294
  }
295

296
  /**
297
   * Client and server requests have to go through different URLs. Call this to get the URL
298
   * appropriate for the current request.
299
   * @returns {string} URL to use for the current request
300
   */
301
  private get baseUrl(): string {
302
    if (this.backend) {
169✔
303
      return SERVER_URL;
43✔
304
    }
305
    return this.isServer ? BACKEND_URL : API_URL;
126✔
306
  }
307

308
  /**
309
   * Build the complete request URL for the given path, appropriate for client and server requests.
310
   * @param {string} path Path to append to the base URL
311
   * @param {boolean} isDbRequest? True to get data from database instead of search engine
312
   * @returns {string} Complete URL for the given path
313
   */
314
  private pathUrl(path: string, isDbRequest = false): string {
93✔
315
    const pathHasQuery = path.includes("?");
169✔
316
    const dbRequestQuery = isDbRequest
169✔
317
      ? `${pathHasQuery ? "&" : "?"}datastore=database`
2✔
318
      : "";
319
    return `${this.baseUrl}${path}${dbRequestQuery}`;
169✔
320
  }
321

322
  /**
323
   * Build the options object for a fetch() request, including the headers.
324
   * @param {string} method Method to use for the request
325
   * @param {object} options Unnamed parameter indicating request options
326
   * @param {object} options.payload? Object to send as the request body
327
   * @param {string} options.accept? Accept header to send with the request
328
   * @param {string} options.contentType? Content-Type header to send with the request
329
   * @returns {RequestInit} Options object for fetch()
330
   */
331
  private buildOptions(
332
    method: FetchMethod,
333
    additional: {
334
      payload?: object;
335
      accept?: string;
336
      range?: string;
337
      contentType?: string;
338
      acceptEncoding?: string;
339
    },
340
    includeCredentials = true
99✔
341
  ): RequestInit {
342
    if (additional.accept) {
99✔
343
      this.headers.set("Accept", additional.accept);
99✔
344
    }
345
    if (additional.range) {
99!
UNCOV
346
      this.headers.set("Range", additional.range);
×
347
    }
348
    if (additional.contentType) {
99✔
349
      this.headers.set("Content-Type", additional.contentType);
12✔
350
    }
351
    const options: RequestInit = {
99✔
352
      method,
353
      redirect: "follow",
354
      headers: this.headers,
355
      ...(includeCredentials && { credentials: "include" }),
198✔
356
    };
357
    if (additional.payload && METHODS_ALLOWING_BODY.includes(method)) {
99✔
358
      options.body = JSON.stringify(additional.payload);
12✔
359
    }
360
    return options;
99✔
361
  }
362

363
  /**
364
   * Request the object with the given path.
365
   * @param {string} path Path to requested resource
366
   * @param {object} options? indicating request options
367
   * @param {boolean} options.isDbRequest True to get data from database instead of search engine
368
   * @returns {Promise<DataProviderObject|ErrorObject>} Requested object, error object, or
369
   *  `defaultErrorValue` if given and the request fails
370
   */
371
  public async getObject(
372
    path: string,
373
    options = { isDbRequest: false }
74✔
374
  ): Promise<Result<DataProviderObject, ErrorObject>> {
375
    const headerOptions = this.buildOptions("GET", {
76✔
376
      accept: PAYLOAD_FORMAT.JSON,
377
    });
378
    try {
76✔
379
      logRequest("getObject", this.pathUrl(path));
76✔
380
      const response = await fetch(
76✔
381
        this.pathUrl(path, options.isDbRequest),
382
        headerOptions
383
      );
384
      if (!response.ok) {
73✔
385
        const error = {
4✔
386
          ...(await response.json()),
387
          isError: true,
388
        } as ErrorObject;
389
        return err(error);
4✔
390
      }
391
      const results = (await response.json()) as DataProviderObject;
69✔
392
      return ok(results);
69✔
393
    } catch (error) {
394
      console.log("NETWORK ERROR: ", error);
3✔
395
      return err(NETWORK_ERROR_RESPONSE);
3✔
396
    }
397
  }
398

399
  /**
400
   * Request the object with the given URL, including protocol and domain.
401
   * @param {string} url Full URL to requested resource
402
   * @param {string} [accept] Accept header to send with the request; application/json by default
403
   * @returns {Promise<DataProviderObject|ErrorObject>} Requested object or error object
404
   */
405
  public async getObjectByUrl(
406
    url: string
407
  ): Promise<Result<DataProviderObject, ErrorObject>> {
408
    const headerOptions = this.buildOptions("GET", {
6✔
409
      accept: PAYLOAD_FORMAT.JSON,
410
    });
411
    try {
6✔
412
      logRequest("getObjectByUrl", url);
6✔
413
      const response = await fetch(url, headerOptions);
6✔
414
      if (!response.ok) {
5✔
415
        const error = {
2✔
416
          ...(await response.json()),
417
          isError: true,
418
        } as ErrorObject;
419
        return err(error);
2✔
420
      }
421
      const results = (await response.json()) as DataProviderObject;
3✔
422
      return ok(results);
3✔
423
    } catch (error) {
424
      console.log(error);
1✔
425
      return err(NETWORK_ERROR_RESPONSE);
1✔
426
    }
427
  }
428

429
  /**
430
   * Request a number of objects with the given paths, returning each path's resource in an array
431
   * in the same order as their paths in the given array. Any paths that result in an error
432
   * get placed that array entry.
433
   * @param {array} paths Array of paths to requested resources
434
   * @param {object} options? Options for these requests
435
   * @param {boolean} options.filterErrors True to filter errored requests from the returned array
436
   * @returns {Promise<Array<DataProviderObject|ErrorObject|T>>} Array of requested objects
437
   */
438
  public async getMultipleObjects(
439
    paths: Array<string>,
440
    options = { filterErrors: false }
2✔
441
  ): Promise<Array<Result<DataProviderObject, ErrorObject>>> {
442
    logRequest("getMultipleObjects", `[${paths.join(", ")}]`);
3✔
443
    const results =
444
      paths.length > 0
3✔
445
        ? await Promise.all(paths.map((path) => this.getObject(path)))
6✔
446
        : await Promise.resolve([]);
447

448
    return options.filterErrors
3✔
449
      ? results.filter((result) => result.isOk())
3✔
450
      : results;
451
  }
452

453
  /**
454
   * Same as the `getMultipleObjects()` method, but instead of requesting each individual object in
455
   * parallel, it instead requests a `/search`, passing in the `@id` of every requested object, as
456
   * well as the fields needed for each object. This can cause a query string too long to fit in a
457
   * URL, so break the paths into groups of paths, each group mapping to an individual request.
458
   * request. Unlike `getMultipleObjects()`, this method never returns an array that could contain
459
   * entries for failed requests. It instead either returns an array of successfully requested
460
   * objects, or a single error value.
461
   * @param {Array<string>} paths Path of each object to request
462
   * @param {Array<string>} fields Properties of each object to retrieve
463
   * @returns {Promise<Result<Array<DataProviderObject>, ErrorObject>>} Array of requested objects
464
   */
465
  async getMultipleObjectsBulk(
466
    paths: Array<string>,
467
    fields: Array<string>
468
  ): Promise<Result<Array<DataProviderObject>, ErrorObject>> {
469
    logRequest("getMultipleObjectsBulk", `[${paths.join(", ")}]`);
34✔
470

471
    if (paths.length === 0) {
34✔
472
      return ok([]);
1✔
473
    }
474

475
    // Generate the query string for the needed fields of each object.
476
    const fieldQuery = fields.map((field) => `field=${field}`).join("&");
280✔
477

478
    // Break the paths into groups of MAX_PATH_GROUP_SIZE, each group mapping to a data-provider
479
    // request. This reduces the lengths of the query strings to fit within the data provider's
480
    // limits.
481
    const pathGroups = this.pathsIntoPathGroups(paths, fieldQuery.length);
33✔
482

483
    // For each group of paths, request the objects as search results. Send these requests in
484
    // parallel.
485
    const results = await Promise.all(
33✔
486
      pathGroups.map(async (group) => {
487
        const pathQuery = group.map((path) => `@id=${path}`).join("&");
154✔
488
        const query = `${fieldQuery ? `${fieldQuery}&` : ""}${pathQuery}`;
34✔
489
        const response = await this.getObject(
34✔
490
          `/search/?${query}&limit=${group.length}`
491
        );
492
        return response.map((g) => g["@graph"] as Array<DataProviderObject>);
34✔
493
      })
494
    );
495

496
    const firstError = results.find((r) => r.isErr());
34✔
497
    if (firstError !== undefined) {
33✔
498
      // If we found an error, then bail, and we know it's not undefined
499
      return firstError;
1✔
500
    }
501

502
    // We know that all the Results in the results list are Ok
503
    // so we can safely turn them all into Array<DataProviderObject>
504
    // Return the the flattened list wrapped in an Ok
505
    return ok(Ok.all(results).flat());
32✔
506
  }
507

508
  /**
509
   * Request the collection (e.g. "users") with the given path.
510
   * @param {string} collection Name of the collection to request
511
   * @returns {Promise<Result<DataProviderObject, ErrorObject>>} Collection data including all its members in @graph
512
   */
513
  public async getCollection(
514
    collection: string
515
  ): Promise<Result<DataProviderObject, ErrorObject>> {
516
    return this.getObject(`/${collection}/?limit=all`);
1✔
517
  }
518

519
  /**
520
   * Request text file string with the given path.
521
   * @param {string} path Path to the requested resource
522
   * @param {T} defaultErrorValue? Value to return if the request fails; error object if not given
523
   * @returns {Promise<string|DataProviderObject|T>} Requested string, or error object if
524
   *   `defaultErrorValue` not given
525
   */
526
  public async getText<T>(
527
    path: string,
528
    defaultErrorValue?: T
529
  ): Promise<string | ErrorObject | T> {
530
    const options = this.buildOptions("GET", {
5✔
531
      accept: PAYLOAD_FORMAT.TEXT,
532
    });
533
    try {
5✔
534
      logRequest("getText", path);
5✔
535
      const response = await fetch(this.pathUrl(path), options);
5✔
536
      if (!response.ok && defaultErrorValue !== undefined) {
3✔
537
        return defaultErrorValue;
1✔
538
      }
539
      return response.text();
2✔
540
    } catch (error) {
541
      console.log(error);
2✔
542
      return defaultErrorValue === undefined
2✔
543
        ? NETWORK_ERROR_RESPONSE
544
        : defaultErrorValue;
545
    }
546
  }
547

548
  /**
549
   * Request a text file hosted in an AWS bucket with the given full URL. This method returns just
550
   * the first `maxLines` lines of the text file, limited to `MAX_READ_LINES`. We expect
551
   * the file to have gzip compression. Very large files work fine with this method, as we only
552
   * read up to `MAX_READ_LINES` lines of the file, and decompress the file in chunks.
553
   * @param url Full URL to a gzipped text file in an AWS bucket
554
   * @param maxLines Maximum number of lines to return from the text file
555
   * @returns First decompressed `maxLines` lines of the text file
556
   */
557
  /* istanbul ignore next */
558
  public async getZippedPreviewText(url, maxLines = DEFAULT_MAX_TEXT_LINES) {
559
    const options = this.buildOptions(
560
      "GET",
561
      {
562
        accept: PAYLOAD_FORMAT.TEXT,
563
        range: `bytes=0-${MAX_READ_SIZE - 1}`,
564
      },
565
      false
566
    );
567
    const response = await fetch(url, options);
568
    if (!response.body) {
569
      throw new Error("ReadableStream not supported in this browser");
570
    }
571

572
    // Set up for streamed reads and decompression by streamed chunks.
573
    const reader = response.body.getReader();
574
    const inflator = new pako.Inflate({ to: "string" });
575

576
    // Read the stream by chunks and decompress the chunks until it ends or the number of lines of
577
    // text reaches `maxLines`.
578
    let done = false;
579
    let decompressedText = "";
580
    let compressedReadCount = 0;
581
    let lineCount = 0;
582
    while (!done && compressedReadCount < MAX_READ_SIZE) {
583
      const { value, done: readerDone } = await reader.read();
584
      if (value) {
585
        compressedReadCount += value.length;
586
        // Unzip the chunk of text into the pako buffer and add it to our text accumulator. In some
587
        // cases pako can return `undefined`, so ignore those.
588
        inflator.push(value, readerDone);
589
        if (inflator.err) {
590
          done = true;
591
        } else if (inflator.result) {
592
          console.log("**** inflator.result", inflator.result);
593
          decompressedText += inflator.result;
594
          if (decompressedText) {
595
            // If lineCount exceeds maxLines, stop after the `maxLines` line.
596
            const lines = decompressedText.split("\n");
597
            lineCount += lines.length;
598
            if (lineCount >= maxLines) {
599
              // Trim the accumulated decompressed text to just the first `maxLines` lines
600
              const linesToKeep = lines.slice(0, maxLines);
601
              decompressedText = linesToKeep.join("\n");
602
              done = true;
603
            }
604
          }
605

606
          // Clear inflator result to reset state and prevent growing memory usage.
607
          inflator.result = "";
608
        }
609
      }
610
      done = readerDone;
611
    }
612

613
    if (inflator.err) {
614
      console.error(inflator.err);
615
      return `ERROR: ${inflator.msg}`;
616
    }
617

618
    // Now `decompressedText` contains up to `maxLines` lines
619
    const lines = decompressedText.split("\n");
620
    const linesToKeep = lines.slice(0, maxLines);
621
    linesToKeep.forEach((line, index) => {
622
      console.log(`**** Line ${index + 1}: ${line}`);
623
    });
624
    return linesToKeep.join("\n");
625
  }
626

627
  /**
628
   * Send a POST request with the given object.
629
   * @param {string} path Path to resource to post to
630
   * @param {object} payload Object to post
631
   * @returns {Promise<DataProviderObject|ErrorObject>} Response from POST request
632
   */
633
  public async postObject(
634
    path: string,
635
    payload: object
636
  ): Promise<DataProviderObject | ErrorObject> {
637
    logRequest("postObject", path);
6✔
638
    const options = this.buildOptions("POST", {
6✔
639
      accept: PAYLOAD_FORMAT.JSON,
640
      contentType: PAYLOAD_FORMAT.JSON,
641
      payload,
642
    });
643
    try {
6✔
644
      const response = await fetch(this.pathUrl(path), options);
6✔
645
      return response.json();
5✔
646
    } catch (error) {
647
      console.log(error);
1✔
648
      return NETWORK_ERROR_RESPONSE;
1✔
649
    }
650
  }
651

652
  /**
653
   * Write the given object with a PUT request.
654
   * @param {string} path Path to resource to put
655
   * @param {object} payload Object to put at the given path
656
   * @returns {Promise<DataProviderObject|ErrorObject>} Response from PUT request
657
   */
658
  public async putObject(
659
    path: string,
660
    payload: object
661
  ): Promise<DataProviderObject | ErrorObject> {
662
    const options = this.buildOptions("PUT", {
4✔
663
      accept: PAYLOAD_FORMAT.JSON,
664
      contentType: PAYLOAD_FORMAT.JSON,
665
      payload,
666
    });
667
    try {
4✔
668
      logRequest("putObject", path);
4✔
669
      const response = await fetch(this.pathUrl(path), options);
4✔
670
      return response.json();
3✔
671
    } catch (error) {
672
      console.log(error);
1✔
673
      return NETWORK_ERROR_RESPONSE;
1✔
674
    }
675
  }
676

677
  /**
678
   * Patch the object at the given path with the given payload.
679
   * @param {string} path Path to resource to patch
680
   * @param {object} payload Object to merge into patched object
681
   * @returns {DataProviderObject|ErrorObject} Response from PATCH request
682
   */
683
  public async patchObject(
684
    path: string,
685
    payload: object
686
  ): Promise<DataProviderObject | ErrorObject> {
687
    const options = this.buildOptions("PATCH", {
2✔
688
      accept: PAYLOAD_FORMAT.JSON,
689
      contentType: PAYLOAD_FORMAT.JSON,
690
      payload,
691
    });
692
    try {
2✔
693
      logRequest("patchObject", path);
2✔
694
      const response = await fetch(this.pathUrl(path), options);
2✔
695
      return response.json();
1✔
696
    } catch (error) {
697
      console.log(error);
1✔
698
      return NETWORK_ERROR_RESPONSE;
1✔
699
    }
700
  }
701
}
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