@clickup/rest-client 2.10.293 → 2.10.296

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.eslintrc.base.js +20 -2
  2. package/README.md +3 -1
  3. package/SECURITY.md +39 -0
  4. package/dist/.eslintcache +1 -0
  5. package/dist/RestClient.js +1 -1
  6. package/dist/RestClient.js.map +1 -1
  7. package/dist/RestOptions.d.ts +5 -7
  8. package/dist/RestOptions.d.ts.map +1 -1
  9. package/dist/RestOptions.js +2 -2
  10. package/dist/RestOptions.js.map +1 -1
  11. package/dist/RestRequest.d.ts.map +1 -1
  12. package/dist/RestRequest.js +7 -13
  13. package/dist/RestRequest.js.map +1 -1
  14. package/dist/RestResponse.d.ts +4 -1
  15. package/dist/RestResponse.d.ts.map +1 -1
  16. package/dist/RestResponse.js +2 -1
  17. package/dist/RestResponse.js.map +1 -1
  18. package/dist/RestStream.js.map +1 -1
  19. package/dist/errors/RestRateLimitError.d.ts.map +1 -1
  20. package/dist/errors/RestRateLimitError.js.map +1 -1
  21. package/dist/errors/RestRetriableError.d.ts.map +1 -1
  22. package/dist/errors/RestRetriableError.js.map +1 -1
  23. package/dist/errors/RestTokenInvalidError.d.ts.map +1 -1
  24. package/dist/errors/RestTokenInvalidError.js.map +1 -1
  25. package/dist/helpers/depaginate.d.ts +1 -1
  26. package/dist/helpers/depaginate.d.ts.map +1 -1
  27. package/dist/helpers/depaginate.js.map +1 -1
  28. package/dist/internal/RestFetchReader.d.ts +9 -2
  29. package/dist/internal/RestFetchReader.d.ts.map +1 -1
  30. package/dist/internal/RestFetchReader.js +14 -3
  31. package/dist/internal/RestFetchReader.js.map +1 -1
  32. package/dist/internal/RestRangeUploader.js.map +1 -1
  33. package/dist/internal/calcRetryDelay.js.map +1 -1
  34. package/dist/internal/inferResBodyEncoding.js.map +1 -1
  35. package/dist/internal/inspectPossibleJSON.js.map +1 -1
  36. package/dist/internal/substituteParams.js.map +1 -1
  37. package/dist/internal/throwIfErrorResponse.js.map +1 -1
  38. package/dist/middlewares/paceRequests.js.map +1 -1
  39. package/dist/pacers/PacerQPS.js.map +1 -1
  40. package/docs/README.md +3 -1
  41. package/docs/classes/PacerComposite.md +7 -3
  42. package/docs/classes/PacerQPS.md +7 -3
  43. package/docs/classes/RestClient.md +32 -28
  44. package/docs/classes/RestContentSizeOverLimitError.md +5 -1
  45. package/docs/classes/RestError.md +5 -1
  46. package/docs/classes/RestRateLimitError.md +6 -2
  47. package/docs/classes/RestRequest.md +22 -18
  48. package/docs/classes/RestResponse.md +25 -10
  49. package/docs/classes/RestResponseError.md +5 -1
  50. package/docs/classes/RestRetriableError.md +6 -2
  51. package/docs/classes/RestStream.md +12 -8
  52. package/docs/classes/RestTimeoutError.md +5 -1
  53. package/docs/classes/RestTokenInvalidError.md +6 -2
  54. package/docs/interfaces/Middleware.md +4 -4
  55. package/docs/interfaces/Pacer.md +2 -2
  56. package/docs/interfaces/PacerQPSBackend.md +2 -2
  57. package/docs/interfaces/RestLogEvent.md +1 -1
  58. package/docs/interfaces/RestOptions.md +59 -23
  59. package/docs/interfaces/TokenGetter.md +3 -3
  60. package/docs/modules.md +4 -4
  61. package/jest.config.js +3 -0
  62. package/package.json +40 -8
  63. package/src/RestClient.ts +490 -0
  64. package/src/RestOptions.ts +186 -0
  65. package/src/RestRequest.ts +441 -0
  66. package/src/RestResponse.ts +49 -0
  67. package/src/RestStream.ts +89 -0
  68. package/src/errors/RestContentSizeOverLimitError.ts +3 -0
  69. package/src/errors/RestError.ts +6 -0
  70. package/src/errors/RestRateLimitError.ts +12 -0
  71. package/src/errors/RestResponseError.ts +46 -0
  72. package/src/errors/RestRetriableError.ts +12 -0
  73. package/src/errors/RestTimeoutError.ts +3 -0
  74. package/src/errors/RestTokenInvalidError.ts +11 -0
  75. package/src/helpers/depaginate.ts +37 -0
  76. package/src/index.ts +50 -0
  77. package/src/internal/RestFetchReader.ts +188 -0
  78. package/src/internal/RestRangeUploader.ts +61 -0
  79. package/src/internal/calcRetryDelay.ts +59 -0
  80. package/src/internal/inferResBodyEncoding.ts +33 -0
  81. package/src/internal/inspectPossibleJSON.ts +71 -0
  82. package/src/internal/prependNewlineIfMultiline.ts +3 -0
  83. package/src/internal/substituteParams.ts +25 -0
  84. package/src/internal/throwIfErrorResponse.ts +89 -0
  85. package/src/internal/toFloatMs.ts +3 -0
  86. package/src/middlewares/paceRequests.ts +42 -0
  87. package/src/pacers/Pacer.ts +22 -0
  88. package/src/pacers/PacerComposite.ts +29 -0
  89. package/src/pacers/PacerQPS.ts +147 -0
  90. package/tsconfig.json +5 -13
  91. package/typedoc.json +6 -1
  92. package/dist/tsconfig.tsbuildinfo +0 -1
@@ -0,0 +1,89 @@
1
+ import { Memoize } from "fast-typescript-memoize";
2
+ import type RestResponse from "./RestResponse";
3
+
4
+ /**
5
+ * Once created, RestStream must be iterated in full, otherwise the connection
6
+ * will remain dangling. Also, this class is where we hide the details of the
7
+ * actual stream reading using AsyncGenerator bridge abstraction.
8
+ *
9
+ * RestStream can also read binary data depending on the Content-Type response
10
+ * header and/or the charset provided there. The binary data is still returned
11
+ * as a string, one string character per each byte. To convert it to a Buffer,
12
+ * use something like `Buffer.from(responseText, "binary")`.
13
+ */
14
+ export default class RestStream {
15
+ private _generator?: AsyncGenerator<string, void>;
16
+
17
+ constructor(
18
+ public readonly res: RestResponse,
19
+ readerIterable: {
20
+ [Symbol.asyncIterator]: () => AsyncGenerator<string, void>;
21
+ },
22
+ ) {
23
+ this._generator = readerIterable[Symbol.asyncIterator]();
24
+ }
25
+
26
+ /**
27
+ * Reads the prefix of the stream. Closes the connection after the read is
28
+ * done in all cases, so safe to be used to e.g. receive a trimmed response.
29
+ */
30
+ async consumeReturningPrefix(maxChars: number): Promise<string> {
31
+ const text: string[] = [];
32
+ let length = 0;
33
+ for await (const chunk of this) {
34
+ // According to Google, in v8 string concatenation is as efficient as
35
+ // array or buffer joining.
36
+ text.push(chunk);
37
+ length += chunk.length;
38
+ if (length >= maxChars) {
39
+ break;
40
+ }
41
+ }
42
+
43
+ return text.join("").substring(0, maxChars);
44
+ }
45
+
46
+ /**
47
+ * Closes the connection.
48
+ */
49
+ async close(): Promise<void> {
50
+ // First, try to interrupt the active iteration, if any.
51
+ await this[Symbol.asyncIterator]().return();
52
+ // It is possible that this.[Symbol.asyncIterator] has never been iterated
53
+ // before at all, so its `finally` got never executed. This happens when
54
+ // RestFetchReader#preload() consumed some small chunk of data, and then
55
+ // no-one iterated the remaining body in RestStream, they just called
56
+ // close() on it. So we recheck & close the RestFetchReader iterator if it
57
+ // is still there.
58
+ const generator = this._generator;
59
+ if (generator) {
60
+ delete this._generator;
61
+ await generator.return();
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Allows to iterate over the entire stream of data. You must consume the
67
+ * entire iterable or at least call this.close(), otherwise the connection may
68
+ * remain open.
69
+ */
70
+ @Memoize()
71
+ async *[Symbol.asyncIterator](): AsyncGenerator<string, void> {
72
+ const generator = this._generator;
73
+ if (!generator) {
74
+ return;
75
+ }
76
+
77
+ delete this._generator;
78
+ try {
79
+ yield this.res.text;
80
+ for await (const chunk of generator) {
81
+ yield chunk;
82
+ }
83
+ } finally {
84
+ // The code enters here if the caller interrupted (returned) the iteration
85
+ // after receiving the 1st textFetched chunk.
86
+ await generator.return();
87
+ }
88
+ }
89
+ }
@@ -0,0 +1,3 @@
1
+ import RestResponseError from "./RestResponseError";
2
+
3
+ export default class RestContentSizeOverLimitError extends RestResponseError {}
@@ -0,0 +1,6 @@
1
+ export default class RestError extends Error {
2
+ constructor(message: string) {
3
+ super(message);
4
+ this.name = this.constructor.name; // https://javascript.info/custom-errors#further-inheritance
5
+ }
6
+ }
@@ -0,0 +1,12 @@
1
+ import type RestResponse from "../RestResponse";
2
+ import RestResponseError from "./RestResponseError";
3
+
4
+ export default class RestRateLimitError extends RestResponseError {
5
+ constructor(
6
+ message: string,
7
+ public delayMs: number,
8
+ res: RestResponse,
9
+ ) {
10
+ super(message, res);
11
+ }
12
+ }
@@ -0,0 +1,46 @@
1
+ import inspectPossibleJSON from "../internal/inspectPossibleJSON";
2
+ import prependNewlineIfMultiline from "../internal/prependNewlineIfMultiline";
3
+ import type RestResponse from "../RestResponse";
4
+ import RestError from "./RestError";
5
+
6
+ const RESPONSE_MAX_LEN_IN_ERROR =
7
+ process.env["NODE_ENV"] === "development" ? Number.MAX_SAFE_INTEGER : 2048;
8
+
9
+ export default class RestResponseError extends RestError {
10
+ readonly res!: RestResponse;
11
+
12
+ readonly method: string;
13
+ readonly host: string;
14
+ readonly pathname: string;
15
+ readonly requestArgs: string;
16
+ readonly requestBody: string;
17
+ readonly responseHeaders: string;
18
+
19
+ constructor(message: string, res: RestResponse) {
20
+ super(
21
+ `HTTP ${res.status}: ` +
22
+ (message ? `${message}: ` : "") +
23
+ prependNewlineIfMultiline(
24
+ inspectPossibleJSON(res.headers, res.text, RESPONSE_MAX_LEN_IN_ERROR),
25
+ ),
26
+ );
27
+ Object.defineProperty(this, "res", { value: res, enumerable: false }); // hidden from inspect()
28
+ const url = new URL(res.req.url);
29
+ const search = (url.search || "").replace(/^\?/, "");
30
+ this.method = res.req.method;
31
+ this.host = res.req.headers.get("host") || url.host;
32
+ this.pathname = url.pathname;
33
+ this.requestArgs = search.replace(/(?<=[&])/g, "\n");
34
+ this.requestBody = inspectPossibleJSON(
35
+ res.headers,
36
+ res.req.body,
37
+ RESPONSE_MAX_LEN_IN_ERROR,
38
+ );
39
+ this.responseHeaders = [...res.headers]
40
+ .map(([name, value]) => `${name}: ${value}`)
41
+ .join("\n");
42
+ // Don't expose query string parameters in the message as they may contain
43
+ // sensitive information.
44
+ this.message += `\n ${this.method} ${url.protocol}//${url.host}${url.pathname}`;
45
+ }
46
+ }
@@ -0,0 +1,12 @@
1
+ import type RestResponse from "../RestResponse";
2
+ import RestResponseError from "./RestResponseError";
3
+
4
+ export default class RestRetriableError extends RestResponseError {
5
+ constructor(
6
+ message: string,
7
+ public delayMs: number,
8
+ res: RestResponse,
9
+ ) {
10
+ super(message, res);
11
+ }
12
+ }
@@ -0,0 +1,3 @@
1
+ import RestResponseError from "./RestResponseError";
2
+
3
+ export default class RestTimeoutError extends RestResponseError {}
@@ -0,0 +1,11 @@
1
+ import type RestResponse from "../RestResponse";
2
+ import RestResponseError from "./RestResponseError";
3
+
4
+ export default class RestTokenInvalidError extends RestResponseError {
5
+ constructor(
6
+ public readonly humanReason: string,
7
+ res: RestResponse,
8
+ ) {
9
+ super(humanReason, res);
10
+ }
11
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Keeps calling a function with an updating cursor, and depaginates all the
3
+ * results until the cursor returned is null or undefined.
4
+ *
5
+ * On each call, the inner function needs to return an array with two elements:
6
+ * 1. Array or results, which could be empty, but not null or undefined.
7
+ * 2. A new cursor.
8
+ */
9
+ export default async function* depaginate<TItem, TCursor = string>(
10
+ readFunc: (
11
+ cursor: TCursor | undefined,
12
+ ) => Promise<readonly [TItem[], TCursor | null | undefined]>,
13
+ ): AsyncGenerator<TItem, void, undefined> {
14
+ let prevCursor: TCursor | null | undefined = undefined;
15
+ let cursor: TCursor | null | undefined = undefined;
16
+ for (;;) {
17
+ let items: TItem[];
18
+ [items, cursor] = await readFunc(cursor === null ? undefined : cursor);
19
+ yield* items;
20
+
21
+ if (cursor === null || cursor === undefined) {
22
+ break;
23
+ }
24
+
25
+ if (JSON.stringify(prevCursor) === JSON.stringify(cursor)) {
26
+ throw Error(
27
+ "Depagination got stuck: prevCursor=" +
28
+ JSON.stringify(prevCursor) +
29
+ ", cursor=" +
30
+ JSON.stringify(cursor) +
31
+ " (they must differ)",
32
+ );
33
+ }
34
+
35
+ prevCursor = cursor;
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,50 @@
1
+ import { Headers } from "node-fetch";
2
+ import RestContentSizeOverLimitError from "./errors/RestContentSizeOverLimitError";
3
+ import RestError from "./errors/RestError";
4
+ import RestRateLimitError from "./errors/RestRateLimitError";
5
+ import RestResponseError from "./errors/RestResponseError";
6
+ import RestRetriableError from "./errors/RestRetriableError";
7
+ import RestTimeoutError from "./errors/RestTimeoutError";
8
+ import RestTokenInvalidError from "./errors/RestTokenInvalidError";
9
+ import depaginate from "./helpers/depaginate";
10
+ import paceRequests from "./middlewares/paceRequests";
11
+ import Pacer from "./pacers/Pacer";
12
+ import PacerComposite from "./pacers/PacerComposite";
13
+ import PacerQPS, { PacerQPSBackend } from "./pacers/PacerQPS";
14
+ import RestClient, { TokenGetter } from "./RestClient";
15
+ import RestOptions, {
16
+ RestLogEvent,
17
+ Middleware,
18
+ Agents,
19
+ DEFAULT_OPTIONS,
20
+ } from "./RestOptions";
21
+ import RestRequest from "./RestRequest";
22
+ import RestResponse from "./RestResponse";
23
+ import RestStream from "./RestStream";
24
+
25
+ export {
26
+ Agents,
27
+ DEFAULT_OPTIONS,
28
+ depaginate,
29
+ Headers,
30
+ Middleware,
31
+ Pacer,
32
+ PacerComposite,
33
+ paceRequests,
34
+ PacerQPS,
35
+ PacerQPSBackend,
36
+ RestClient,
37
+ RestContentSizeOverLimitError,
38
+ RestError,
39
+ RestLogEvent,
40
+ RestOptions,
41
+ RestRateLimitError,
42
+ RestRequest,
43
+ RestResponse,
44
+ RestResponseError,
45
+ RestRetriableError,
46
+ RestStream,
47
+ RestTimeoutError,
48
+ RestTokenInvalidError,
49
+ TokenGetter,
50
+ };
@@ -0,0 +1,188 @@
1
+ import type { Agent as HttpAgent } from "http";
2
+ import AbortControllerPolyfilled from "abort-controller";
3
+ import { Memoize } from "fast-typescript-memoize";
4
+ import type { RequestInit } from "node-fetch";
5
+ import fetch, { Headers, Request } from "node-fetch";
6
+ import inferResBodyEncoding from "./inferResBodyEncoding";
7
+
8
+ export interface RestFetchReaderOptions {
9
+ timeoutMs?: number;
10
+ heartbeat?: () => Promise<void>;
11
+ onTimeout?: (reader: RestFetchReader, e: any) => void;
12
+ onAfterRead?: (reader: RestFetchReader) => void;
13
+ }
14
+
15
+ /**
16
+ * A low-level stateful reader engine on top of node-fetch which implements
17
+ * "preload first N chars and then leave the rest ready for iteration" pattern,
18
+ * with global timeout for the entire fetching time.
19
+ *
20
+ * Once created, the object MUST be iterated in full to consume the rest of the
21
+ * stream and close the connection. In case you're not interested in its entire
22
+ * content, you must prematurely "return" (close) the iterator.
23
+ *
24
+ * The abstraction is intentionally kept independent on all others, to make it
25
+ * simple and testable separately.
26
+ */
27
+ export default class RestFetchReader {
28
+ private _status = 0;
29
+ private _headers = new Headers();
30
+ private _textFetched = "";
31
+ private _textIsPartial = true;
32
+ private _charsRead = 0;
33
+
34
+ constructor(
35
+ private _url: string,
36
+ private _reqInit: RequestInit,
37
+ private _options: RestFetchReaderOptions,
38
+ ) {}
39
+
40
+ /**
41
+ * Returns the number of characters read from the stream so far.
42
+ */
43
+ get charsRead() {
44
+ return this._charsRead;
45
+ }
46
+
47
+ /**
48
+ * Returns the Agent instance used for this request. It's implied that
49
+ * RestRequest#agent always points to a http.Agent object.
50
+ */
51
+ get agent() {
52
+ return (
53
+ this._reqInit.agent &&
54
+ typeof this._reqInit.agent === "object" &&
55
+ "sockets" in this._reqInit.agent
56
+ ? this._reqInit.agent
57
+ : null
58
+ ) as HttpAgent | null;
59
+ }
60
+
61
+ /**
62
+ * Returns HTTP status after preload() was called.
63
+ */
64
+ get status() {
65
+ return this._status;
66
+ }
67
+
68
+ /**
69
+ * Returns HTTP headers after preload() was called.
70
+ */
71
+ get headers() {
72
+ return this._headers;
73
+ }
74
+
75
+ /**
76
+ * Returns the data preloaded so far.
77
+ */
78
+ get textFetched(): string {
79
+ return this._textFetched;
80
+ }
81
+
82
+ /**
83
+ * If true, then there is a chance that reading more from the stream will
84
+ * return more data.
85
+ */
86
+ get textIsPartial() {
87
+ return this._textIsPartial;
88
+ }
89
+
90
+ /**
91
+ * Reads preloadChars chars or a little bit more from the response and puts
92
+ * them to this.textFetched. Leaves the rest of the data in res.body for
93
+ * future reads if there are more data to fetch (you must consume them or
94
+ * close the stream, otherwise the connection will remain open).
95
+ */
96
+ async preload(preloadChars: number) {
97
+ const generator = this[Symbol.asyncIterator]();
98
+ try {
99
+ while (this._charsRead < preloadChars) {
100
+ const { value, done } = await generator.next();
101
+ if (done) {
102
+ this._textIsPartial = false;
103
+ await generator.return();
104
+ return;
105
+ }
106
+
107
+ this._textFetched += value;
108
+ }
109
+ } catch (e: unknown) {
110
+ await generator.return();
111
+ throw e;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Closes the connection.
117
+ */
118
+ async close() {
119
+ await this[Symbol.asyncIterator]().return();
120
+ }
121
+
122
+ /**
123
+ * Returns an async generator for the rest of the data. Must be consumed
124
+ * entirely, otherwise the connection may remain dangling.
125
+ *
126
+ * Memoization is important here, to return the same generator when we call
127
+ * this method multiple times and to not start a new iteration over and over.
128
+ */
129
+ @Memoize()
130
+ async *[Symbol.asyncIterator]() {
131
+ const { timeoutMs, onTimeout, onAfterRead } = this._options;
132
+
133
+ // Some of react-client users are still on v14 node.
134
+ const controller =
135
+ typeof AbortController === "undefined"
136
+ ? new AbortControllerPolyfilled()
137
+ : new AbortController();
138
+
139
+ const timeout = timeoutMs
140
+ ? setTimeout(() => controller.abort(), timeoutMs)
141
+ : undefined;
142
+
143
+ try {
144
+ // DO NOT use fetch(fetchReq) with one argument! It clones the stream
145
+ // which just doesn't work in practice, even with file streams. I wasted
146
+ // 4h on debugging this: fetch(fetchReq.url, fetchReq) works and
147
+ // fetch(fetchReq) doesn't for e.g. Dropbox API and
148
+ // https://stackoverflow.com/a/44577569
149
+ const res = await fetch(
150
+ this._url,
151
+ new Request(this._url, {
152
+ ...this._reqInit,
153
+ signal: controller.signal as any,
154
+ }),
155
+ );
156
+ this._status = res.status;
157
+ this._headers = res.headers;
158
+
159
+ // See https://nodejs.org/api/stream.html#readablesetencodingencoding on
160
+ // how Node streams and setEncoding() handle decoding when the returned
161
+ // chunks cross the boundaries of multi-byte characters (TL;DR: it works
162
+ // fine, that's why we work with string and not Buffer here).
163
+ res.body.setEncoding(inferResBodyEncoding(res));
164
+
165
+ await this._options.heartbeat?.();
166
+ for await (const chunk of res.body) {
167
+ await this._options.heartbeat?.();
168
+ this._charsRead += chunk.length;
169
+ yield chunk as string;
170
+ onAfterRead?.(this);
171
+ }
172
+ } catch (e: unknown) {
173
+ if (controller.signal.aborted && onTimeout) {
174
+ onTimeout(this, e);
175
+ } else {
176
+ throw e;
177
+ }
178
+ } finally {
179
+ timeout && clearTimeout(timeout);
180
+ // If someone stops iterating prematurely, we forcefully close the
181
+ // connection in all cases. Theoretically, stopping the iteration on
182
+ // res.body should've closed the connection, but in practice it doesn't
183
+ // happen; it looks like a bug in node-fetch, and thus, we must always use
184
+ // the AbortController in the end.
185
+ controller.abort();
186
+ }
187
+ }
188
+ }
@@ -0,0 +1,61 @@
1
+ import type RestClient from "../RestClient";
2
+
3
+ /**
4
+ * Sends a series of Content-Range requests to an URL.
5
+ * - The stream size is unknown in advance even theoretically. So we read it
6
+ * with chunkSize+1 bytes chunks (+1 is to know for sure, is there something
7
+ * else left in the stream or not) and then send data with chunkSize bytes
8
+ * chunks.
9
+ * - The last chunk is a terminating one (and we know, which one is the last),
10
+ * so we reflect it in "Content-Range: x-y/S" format setting S to the total
11
+ * number of bytes in the stream.
12
+ */
13
+ export default class RestRangeUploader {
14
+ private _pos = 0;
15
+
16
+ constructor(
17
+ private _client: RestClient,
18
+ private _chunkSize: number,
19
+ private _method: "POST" | "PUT",
20
+ private _path: string,
21
+ private _mimeType: string,
22
+ ) {}
23
+
24
+ async upload(stream: AsyncIterable<Buffer>) {
25
+ let buf = Buffer.allocUnsafe(0);
26
+ let res: string | null = null;
27
+ for await (const readData of stream) {
28
+ buf = Buffer.concat([buf, readData]);
29
+ while (buf.length >= this._chunkSize + 1) {
30
+ res = await this._flush(buf.slice(0, this._chunkSize), false);
31
+ buf = Buffer.from(buf.slice(this._chunkSize));
32
+ }
33
+ // After this `while` loop finishes, there is always something left in buf
34
+ // (due to the +1 trick). It guarantees that we have a chance to call
35
+ // flush(..., true) for the very last chunk.
36
+ }
37
+
38
+ if (buf.length > 0) {
39
+ res = await this._flush(buf, true);
40
+ }
41
+
42
+ return res;
43
+ }
44
+
45
+ private async _flush(buf: Buffer, isLast: boolean) {
46
+ if (buf.length === 0) {
47
+ return null;
48
+ }
49
+
50
+ const totalSize = isLast ? this._pos + buf.length : "*";
51
+ const res = await this._client
52
+ .writeRaw(this._path, buf, this._mimeType, this._method, "*/*")
53
+ .setHeader(
54
+ "Content-Range",
55
+ `bytes ${this._pos}-${this._pos + buf.length - 1}/${totalSize}`,
56
+ )
57
+ .text();
58
+ this._pos += buf.length;
59
+ return res;
60
+ }
61
+ }
@@ -0,0 +1,59 @@
1
+ import RestContentSizeOverLimitError from "../errors/RestContentSizeOverLimitError";
2
+ import RestRateLimitError from "../errors/RestRateLimitError";
3
+ import RestRetriableError from "../errors/RestRetriableError";
4
+ import RestTokenInvalidError from "../errors/RestTokenInvalidError";
5
+ import type RestOptions from "../RestOptions";
6
+ import type RestResponse from "../RestResponse";
7
+
8
+ /**
9
+ * Returns a new retry delay of the error needs to be retried, otherwise
10
+ * "no_retry".
11
+ */
12
+ export default function calcRetryDelay(
13
+ error: any,
14
+ options: RestOptions,
15
+ res: RestResponse,
16
+ retryDelayMs: number,
17
+ ): number | "no_retry" {
18
+ if (
19
+ error instanceof RestRateLimitError ||
20
+ error instanceof RestRetriableError
21
+ ) {
22
+ // We've already made a decision to retry this error.
23
+ return Math.min(
24
+ options.retryDelayMaxMs,
25
+ Math.max(retryDelayMs, error.delayMs),
26
+ );
27
+ }
28
+
29
+ switch (options.isRetriableError(res, error)) {
30
+ case "RETRY":
31
+ default:
32
+ break; // number returned
33
+
34
+ case "BEST_EFFORT":
35
+ if (error instanceof RestTokenInvalidError) {
36
+ return "no_retry";
37
+ }
38
+
39
+ if (
40
+ !(error instanceof RestRateLimitError) &&
41
+ res.status >= 400 &&
42
+ res.status <= 499
43
+ ) {
44
+ return "no_retry";
45
+ }
46
+
47
+ if (error instanceof RestContentSizeOverLimitError) {
48
+ // Content size ... over limit.
49
+ return "no_retry";
50
+ }
51
+
52
+ break;
53
+
54
+ case "NEVER_RETRY":
55
+ return "no_retry";
56
+ }
57
+
58
+ return retryDelayMs;
59
+ }
@@ -0,0 +1,33 @@
1
+ import type { Response } from "node-fetch";
2
+
3
+ const CHARSET_RE =
4
+ /(?:charset|encoding)\s{0,10}=\s{0,10}['"]? {0,10}([-\w]{1,100})/i;
5
+ const BUFFER_ENCODINGS = ["ascii", "utf8", "utf-8", "utf16le", "ucs2", "ucs-2"];
6
+
7
+ /**
8
+ * Tries its best to infer the encoding of the Response, falling back to UTF-8
9
+ * as an opinionated default value on failure.
10
+ */
11
+ export default function inferResBodyEncoding(res: Response): BufferEncoding {
12
+ const contentType = res.headers.get("content-type")?.toLowerCase();
13
+ const charset = contentType?.match(CHARSET_RE)
14
+ ? RegExp.$1.toLowerCase()
15
+ : undefined;
16
+ return contentType?.startsWith("application/octet-stream")
17
+ ? // It's a binary Content-Type.
18
+ "binary"
19
+ : charset && !BUFFER_ENCODINGS.includes(charset)
20
+ ? // The charset is provided in Content-Type, but unknown by Buffer.
21
+ "binary"
22
+ : charset && BUFFER_ENCODINGS.includes(charset)
23
+ ? // Charset is provided in Content-Type header, and Buffer knows
24
+ // how to decode it.
25
+ (charset as BufferEncoding)
26
+ : // An opinionated choice is made here to always default-decode the
27
+ // response stream as UTF-8. This is because JSON is by definition a UTF-8
28
+ // stream, and people often time respond with JSONs forgetting to provide
29
+ // "; charset=utf-8" part of the Content-Type header (or they forget
30
+ // Content-Type header at all, or put some wrong value as "text/plain"
31
+ // there; there is an endless list of mistake variations here).
32
+ "utf-8";
33
+ }