@clickup/rest-client 2.10.296 → 2.11.1

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 (111) hide show
  1. package/.eslintrc.base.js +10 -1
  2. package/.eslintrc.js +4 -4
  3. package/.github/workflows/ci.yml +26 -0
  4. package/.github/workflows/semgrep.yml +36 -0
  5. package/.prettierrc +8 -0
  6. package/.vscode/extensions.json +8 -0
  7. package/.vscode/tasks.json +20 -0
  8. package/babel.config.js +5 -0
  9. package/dist/.eslintcache +1 -1
  10. package/dist/RestClient.d.ts +1 -3
  11. package/dist/RestClient.d.ts.map +1 -1
  12. package/dist/RestClient.js +3 -4
  13. package/dist/RestClient.js.map +1 -1
  14. package/dist/RestOptions.d.ts +2 -2
  15. package/dist/RestOptions.d.ts.map +1 -1
  16. package/dist/RestOptions.js +1 -0
  17. package/dist/RestOptions.js.map +1 -1
  18. package/dist/RestRequest.d.ts +0 -2
  19. package/dist/RestRequest.d.ts.map +1 -1
  20. package/dist/RestRequest.js +12 -5
  21. package/dist/RestRequest.js.map +1 -1
  22. package/dist/RestResponse.d.ts +0 -1
  23. package/dist/RestResponse.d.ts.map +1 -1
  24. package/dist/helpers/depaginate.js +1 -1
  25. package/dist/helpers/depaginate.js.map +1 -1
  26. package/dist/index.d.ts +2 -5
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +1 -5
  29. package/dist/index.js.map +1 -1
  30. package/dist/internal/RestFetchReader.d.ts +3 -3
  31. package/dist/internal/RestFetchReader.d.ts.map +1 -1
  32. package/dist/internal/RestFetchReader.js +21 -11
  33. package/dist/internal/RestFetchReader.js.map +1 -1
  34. package/dist/internal/RestRangeUploader.d.ts +0 -1
  35. package/dist/internal/RestRangeUploader.d.ts.map +1 -1
  36. package/dist/internal/calcRetryDelay.js +1 -1
  37. package/dist/internal/calcRetryDelay.js.map +1 -1
  38. package/dist/internal/ellipsis.d.ts +6 -0
  39. package/dist/internal/ellipsis.d.ts.map +1 -0
  40. package/dist/internal/ellipsis.js +17 -0
  41. package/dist/internal/ellipsis.js.map +1 -0
  42. package/dist/internal/inferResBodyEncoding.d.ts +0 -1
  43. package/dist/internal/inferResBodyEncoding.d.ts.map +1 -1
  44. package/dist/internal/inferResBodyEncoding.js +1 -1
  45. package/dist/internal/inferResBodyEncoding.js.map +1 -1
  46. package/dist/internal/inspectPossibleJSON.d.ts +0 -2
  47. package/dist/internal/inspectPossibleJSON.d.ts.map +1 -1
  48. package/dist/internal/inspectPossibleJSON.js +6 -9
  49. package/dist/internal/inspectPossibleJSON.js.map +1 -1
  50. package/dist/internal/prependNewlineIfMultiline.js +1 -1
  51. package/dist/internal/prependNewlineIfMultiline.js.map +1 -1
  52. package/dist/internal/substituteParams.js +1 -1
  53. package/dist/internal/substituteParams.js.map +1 -1
  54. package/dist/internal/throwIfErrorResponse.js +2 -2
  55. package/dist/internal/throwIfErrorResponse.js.map +1 -1
  56. package/dist/internal/toFloatMs.js +1 -1
  57. package/dist/internal/toFloatMs.js.map +1 -1
  58. package/dist/middlewares/paceRequests.d.ts +21 -2
  59. package/dist/middlewares/paceRequests.d.ts.map +1 -1
  60. package/dist/middlewares/paceRequests.js +4 -3
  61. package/dist/middlewares/paceRequests.js.map +1 -1
  62. package/docs/interfaces/Pacer.md +7 -12
  63. package/docs/interfaces/PacerOutcome.md +25 -0
  64. package/docs/interfaces/RestOptions.md +22 -47
  65. package/docs/modules.md +4 -7
  66. package/internal/clean.sh +4 -0
  67. package/internal/deploy.sh +7 -0
  68. package/internal/docs.sh +6 -0
  69. package/internal/lint.sh +4 -0
  70. package/jest.config.base.js +18 -0
  71. package/jest.config.js +1 -10
  72. package/package.json +15 -10
  73. package/src/RestClient.ts +2 -2
  74. package/src/RestOptions.ts +3 -0
  75. package/src/RestRequest.ts +32 -5
  76. package/src/__tests__/RestClient.test.ts +53 -0
  77. package/src/__tests__/RestFetchReader.test.ts +154 -0
  78. package/src/__tests__/RestRequest.test.ts +262 -0
  79. package/src/__tests__/RestRequestCacheableLookup.test.ts +88 -0
  80. package/src/__tests__/RestStream.test.ts +67 -0
  81. package/src/__tests__/helpers.ts +173 -0
  82. package/src/index.ts +2 -7
  83. package/src/internal/RestFetchReader.ts +4 -1
  84. package/src/internal/ellipsis.ts +16 -0
  85. package/src/internal/inspectPossibleJSON.ts +1 -5
  86. package/src/internal/throwIfErrorResponse.ts +1 -1
  87. package/src/middlewares/paceRequests.ts +27 -2
  88. package/tsconfig.base.json +31 -0
  89. package/tsconfig.json +1 -31
  90. package/typedoc.config.js +20 -0
  91. package/dist/pacers/Pacer.d.ts +0 -21
  92. package/dist/pacers/Pacer.d.ts.map +0 -1
  93. package/dist/pacers/Pacer.js +0 -3
  94. package/dist/pacers/Pacer.js.map +0 -1
  95. package/dist/pacers/PacerComposite.d.ts +0 -14
  96. package/dist/pacers/PacerComposite.d.ts.map +0 -1
  97. package/dist/pacers/PacerComposite.js +0 -32
  98. package/dist/pacers/PacerComposite.js.map +0 -1
  99. package/dist/pacers/PacerQPS.d.ts +0 -53
  100. package/dist/pacers/PacerQPS.d.ts.map +0 -1
  101. package/dist/pacers/PacerQPS.js +0 -105
  102. package/dist/pacers/PacerQPS.js.map +0 -1
  103. package/docs/classes/PacerComposite.md +0 -66
  104. package/docs/classes/PacerQPS.md +0 -79
  105. package/docs/interfaces/PacerDelay.md +0 -25
  106. package/docs/interfaces/PacerQPSBackend.md +0 -44
  107. package/docs/interfaces/PacerQPSOptions.md +0 -40
  108. package/src/pacers/Pacer.ts +0 -22
  109. package/src/pacers/PacerComposite.ts +0 -29
  110. package/src/pacers/PacerQPS.ts +0 -147
  111. package/typedoc.json +0 -22
@@ -0,0 +1,262 @@
1
+ import { FetchError, Headers } from "node-fetch";
2
+ import {
3
+ DEFAULT_OPTIONS,
4
+ RestClient,
5
+ RestRateLimitError,
6
+ RestResponseError,
7
+ RestTokenInvalidError,
8
+ } from "..";
9
+ import calcRetryDelay from "../internal/calcRetryDelay";
10
+ import throwIfErrorResponse from "../internal/throwIfErrorResponse";
11
+ import RestResponse from "../RestResponse";
12
+
13
+ const REQUEST = new RestClient().get("https://example.com");
14
+ const SUCCESS_RESPONSE = new RestResponse(
15
+ REQUEST,
16
+ null,
17
+ 200,
18
+ new Headers(),
19
+ "",
20
+ false,
21
+ );
22
+ const ERROR_RESPONSE = new RestResponse(
23
+ REQUEST,
24
+ null,
25
+ 500,
26
+ new Headers(),
27
+ "",
28
+ false,
29
+ );
30
+ const NOT_FOUND_RESPONSE = new RestResponse(
31
+ REQUEST,
32
+ null,
33
+ 404,
34
+ new Headers(),
35
+ "",
36
+ false,
37
+ );
38
+ const TOO_MANY_REQUESTS_RESPONSE = new RestResponse(
39
+ REQUEST,
40
+ null,
41
+ 429,
42
+ new Headers(),
43
+ "",
44
+ false,
45
+ );
46
+
47
+ test("isSuccessResponse", () => {
48
+ expect(
49
+ throwIfErrorResponse(
50
+ { ...DEFAULT_OPTIONS, isSuccessResponse: () => "SUCCESS" },
51
+ ERROR_RESPONSE,
52
+ ),
53
+ ).toBeUndefined();
54
+
55
+ expect(() =>
56
+ throwIfErrorResponse(
57
+ { ...DEFAULT_OPTIONS, isSuccessResponse: () => "BEST_EFFORT" },
58
+ NOT_FOUND_RESPONSE,
59
+ ),
60
+ ).toThrow(/404/);
61
+
62
+ expect(
63
+ throwIfErrorResponse(
64
+ { ...DEFAULT_OPTIONS, isSuccessResponse: () => "BEST_EFFORT" },
65
+ SUCCESS_RESPONSE,
66
+ ),
67
+ ).toBeUndefined();
68
+
69
+ expect(() =>
70
+ throwIfErrorResponse(
71
+ { ...DEFAULT_OPTIONS, isSuccessResponse: () => "THROW" },
72
+ SUCCESS_RESPONSE,
73
+ ),
74
+ ).toThrow(/isSuccessResponse/);
75
+ });
76
+
77
+ test("isRateLimitError", () => {
78
+ expect(() =>
79
+ throwIfErrorResponse(
80
+ { ...DEFAULT_OPTIONS, isRateLimitError: () => "RATE_LIMIT" },
81
+ SUCCESS_RESPONSE,
82
+ ),
83
+ ).toThrow(RestRateLimitError);
84
+
85
+ expect(
86
+ throwIfErrorResponse(
87
+ { ...DEFAULT_OPTIONS, isRateLimitError: () => "BEST_EFFORT" },
88
+ SUCCESS_RESPONSE,
89
+ ),
90
+ ).toBeUndefined();
91
+
92
+ expect(() =>
93
+ throwIfErrorResponse(
94
+ { ...DEFAULT_OPTIONS, isRateLimitError: () => "BEST_EFFORT" },
95
+ TOO_MANY_REQUESTS_RESPONSE,
96
+ ),
97
+ ).toThrow(RestRateLimitError);
98
+
99
+ expect(
100
+ throwIfErrorResponse(
101
+ { ...DEFAULT_OPTIONS, isRateLimitError: () => "SOMETHING_ELSE" },
102
+ SUCCESS_RESPONSE,
103
+ ),
104
+ ).toBeUndefined();
105
+ });
106
+
107
+ test("isTokenInvalidError", () => {
108
+ expect(() =>
109
+ throwIfErrorResponse(
110
+ { ...DEFAULT_OPTIONS, isTokenInvalidError: () => true },
111
+ SUCCESS_RESPONSE,
112
+ ),
113
+ ).toThrow(RestTokenInvalidError);
114
+
115
+ expect(
116
+ throwIfErrorResponse(
117
+ { ...DEFAULT_OPTIONS, isTokenInvalidError: () => false },
118
+ SUCCESS_RESPONSE,
119
+ ),
120
+ ).toBeUndefined();
121
+ });
122
+
123
+ test("isRetriableError", () => {
124
+ expect(() =>
125
+ throwIfErrorResponse(
126
+ { ...DEFAULT_OPTIONS, isRetriableError: () => "RETRY" },
127
+ SUCCESS_RESPONSE,
128
+ ),
129
+ ).toThrow(RestResponseError);
130
+
131
+ expect(
132
+ throwIfErrorResponse(
133
+ { ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
134
+ SUCCESS_RESPONSE,
135
+ ),
136
+ ).toBeUndefined();
137
+
138
+ expect(() =>
139
+ throwIfErrorResponse(
140
+ { ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
141
+ NOT_FOUND_RESPONSE,
142
+ ),
143
+ ).toThrow(RestResponseError);
144
+
145
+ expect(
146
+ throwIfErrorResponse(
147
+ { ...DEFAULT_OPTIONS, isRateLimitError: () => "SOMETHING_ELSE" },
148
+ SUCCESS_RESPONSE,
149
+ ),
150
+ ).toBeUndefined();
151
+
152
+ expect(() =>
153
+ throwIfErrorResponse(
154
+ { ...DEFAULT_OPTIONS, isRetriableError: () => "NEVER_RETRY" },
155
+ NOT_FOUND_RESPONSE,
156
+ ),
157
+ ).toThrow(RestResponseError);
158
+ });
159
+
160
+ test("calcRetryDelay", () => {
161
+ expect(
162
+ calcRetryDelay(
163
+ new RestRateLimitError("", 42, SUCCESS_RESPONSE),
164
+ DEFAULT_OPTIONS,
165
+ SUCCESS_RESPONSE,
166
+ 0,
167
+ ),
168
+ ).toEqual(42);
169
+
170
+ expect(
171
+ calcRetryDelay(
172
+ new RestTokenInvalidError("", ERROR_RESPONSE),
173
+ DEFAULT_OPTIONS,
174
+ ERROR_RESPONSE,
175
+ 0,
176
+ ),
177
+ ).toEqual("no_retry");
178
+
179
+ expect(
180
+ calcRetryDelay(
181
+ new RestTokenInvalidError("", ERROR_RESPONSE),
182
+ { ...DEFAULT_OPTIONS, isRetriableError: () => "RETRY" },
183
+ ERROR_RESPONSE,
184
+ 42,
185
+ ),
186
+ ).toEqual(42);
187
+
188
+ expect(
189
+ calcRetryDelay(
190
+ new RestResponseError("", ERROR_RESPONSE),
191
+ { ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
192
+ ERROR_RESPONSE,
193
+ 42,
194
+ ),
195
+ ).toEqual(42);
196
+
197
+ expect(
198
+ calcRetryDelay(
199
+ new RestResponseError("", NOT_FOUND_RESPONSE),
200
+ { ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
201
+ NOT_FOUND_RESPONSE,
202
+ 42,
203
+ ),
204
+ ).toEqual("no_retry");
205
+
206
+ expect(
207
+ calcRetryDelay(
208
+ new RestRateLimitError("", 42, NOT_FOUND_RESPONSE),
209
+ { ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
210
+ NOT_FOUND_RESPONSE,
211
+ 0,
212
+ ),
213
+ ).toEqual(42);
214
+
215
+ expect(
216
+ calcRetryDelay(
217
+ new FetchError("test", "some"),
218
+ {
219
+ ...DEFAULT_OPTIONS,
220
+ isRetriableError: (_: any, error: any) =>
221
+ error instanceof FetchError ? "NEVER_RETRY" : "RETRY",
222
+ },
223
+ NOT_FOUND_RESPONSE,
224
+ 0,
225
+ ),
226
+ ).toEqual("no_retry");
227
+
228
+ expect(
229
+ calcRetryDelay(
230
+ new FetchError("test", "max-size"),
231
+ { ...DEFAULT_OPTIONS, isRetriableError: () => "BEST_EFFORT" },
232
+ NOT_FOUND_RESPONSE,
233
+ 0,
234
+ ),
235
+ ).toEqual("no_retry");
236
+ });
237
+
238
+ test("when isRateLimitError=YES, isRetriableError=NO doesn't matter, and the request is always retriable", () => {
239
+ expect(() =>
240
+ throwIfErrorResponse(
241
+ {
242
+ ...DEFAULT_OPTIONS,
243
+ isRateLimitError: () => "RATE_LIMIT",
244
+ isRetriableError: () => "NEVER_RETRY",
245
+ },
246
+ SUCCESS_RESPONSE,
247
+ ),
248
+ ).toThrow(RestRateLimitError);
249
+
250
+ expect(
251
+ calcRetryDelay(
252
+ new RestRateLimitError("", 42, SUCCESS_RESPONSE),
253
+ {
254
+ ...DEFAULT_OPTIONS,
255
+ isRateLimitError: () => "RATE_LIMIT",
256
+ isRetriableError: () => "NEVER_RETRY",
257
+ },
258
+ SUCCESS_RESPONSE,
259
+ 0,
260
+ ),
261
+ ).toEqual(42);
262
+ });
@@ -0,0 +1,88 @@
1
+ import { Headers } from "node-fetch";
2
+ import type { RequestInit } from "node-fetch";
3
+ import { server } from "./helpers";
4
+
5
+ let ORIGIN: string;
6
+
7
+ beforeAll(async () => {
8
+ ORIGIN = await new Promise((resolve) => {
9
+ server.listen(0, () =>
10
+ resolve("http://127.0.0.1:" + (server.address() as { port: number }).port),
11
+ );
12
+ });
13
+ });
14
+
15
+ afterAll(async () => {
16
+ await new Promise((resolve) => server.close(resolve));
17
+ });
18
+
19
+ test("request works with family=0 and cacheable-lookup runtime loading", async () => {
20
+ const { RestClient } = require("..");
21
+ const client = new RestClient({ allowInternalIPs: true, family: 0 });
22
+ await expect(client.get(`${ORIGIN}/small`, undefined, "*/*").text()).resolves.toBe(
23
+ "ok",
24
+ );
25
+ });
26
+
27
+ test("lookupAsync call shape depends on family option", async () => {
28
+ type LookupAsync = (
29
+ hostname: string,
30
+ options?: { family: 4 | 6 },
31
+ ) => Promise<{ address: string; family: 4 | 6 }>;
32
+
33
+ const lookupAsyncMock: jest.MockedFunction<LookupAsync> = jest.fn(
34
+ async (_hostname: string, _options?: { family: 4 | 6 }) => ({
35
+ address: "93.184.216.34",
36
+ family: 4 as const,
37
+ }),
38
+ );
39
+
40
+ class MockCacheableLookup {
41
+ lookupAsync = lookupAsyncMock;
42
+ }
43
+
44
+ jest.resetModules();
45
+ jest.doMock("cacheable-lookup", () => ({
46
+ __esModule: true,
47
+ default: MockCacheableLookup,
48
+ }));
49
+
50
+ const { DEFAULT_OPTIONS } = require("..");
51
+ const { default: RestRequest } = require("../RestRequest");
52
+
53
+ lookupAsyncMock.mockClear();
54
+
55
+ const createFetchRequest = async (
56
+ request: unknown,
57
+ ): Promise<RequestInit & { url: string }> =>
58
+ (
59
+ request as {
60
+ _createFetchRequest(): Promise<RequestInit & { url: string }>;
61
+ }
62
+ )._createFetchRequest();
63
+
64
+ await createFetchRequest(
65
+ new RestRequest(
66
+ { ...DEFAULT_OPTIONS, allowInternalIPs: true, family: 0 },
67
+ "GET",
68
+ "http://example.com/test",
69
+ new Headers(),
70
+ "",
71
+ ),
72
+ );
73
+
74
+ await createFetchRequest(
75
+ new RestRequest(
76
+ { ...DEFAULT_OPTIONS, allowInternalIPs: true, family: 4 },
77
+ "GET",
78
+ "http://example.com/test",
79
+ new Headers(),
80
+ "",
81
+ ),
82
+ );
83
+
84
+ expect(lookupAsyncMock).toHaveBeenNthCalledWith(1, "example.com");
85
+ expect(lookupAsyncMock).toHaveBeenNthCalledWith(2, "example.com", {
86
+ family: 4,
87
+ });
88
+ });
@@ -0,0 +1,67 @@
1
+ import Stream from "stream";
2
+ import {
3
+ consumeReadable,
4
+ createRestStream,
5
+ server,
6
+ serverAssertConnectionsCount,
7
+ } from "./helpers";
8
+
9
+ let ORIGIN: string;
10
+
11
+ beforeAll(async () => {
12
+ ORIGIN = await new Promise((resolve) => {
13
+ server.listen(0, () =>
14
+ resolve("http://127.0.0.1:" + (server.address() as any).port),
15
+ );
16
+ });
17
+ });
18
+
19
+ afterAll(async () => {
20
+ await new Promise((resolve) => server.close(resolve));
21
+ });
22
+
23
+ beforeEach(async () => {
24
+ await serverAssertConnectionsCount(0);
25
+ });
26
+
27
+ test("prefix", async () => {
28
+ const stream = await createRestStream(`${ORIGIN}/large`);
29
+ const prefix = await stream.consumeReturningPrefix(42);
30
+ expect(prefix.length).toEqual(42);
31
+ await serverAssertConnectionsCount(0);
32
+ });
33
+
34
+ test("stream_readable_proxy_closes_connection", async () => {
35
+ const stream = await createRestStream(`${ORIGIN}/large`);
36
+ const readable = Stream.Readable.from(stream);
37
+ expect((await consumeReadable(readable)).length).toBeGreaterThan(1024 * 10);
38
+ await serverAssertConnectionsCount(0);
39
+ });
40
+
41
+ test("read_cancelled_after_reading_preloaded_chunk_closes_connection", async () => {
42
+ const stream = await createRestStream(`${ORIGIN}/large`, 42);
43
+ await serverAssertConnectionsCount(1);
44
+
45
+ for await (const chunk of stream) {
46
+ expect(chunk).toEqual(stream.res.text);
47
+ await serverAssertConnectionsCount(1);
48
+ break;
49
+ }
50
+
51
+ await serverAssertConnectionsCount(0);
52
+ });
53
+
54
+ test("read_thrown_after_reading_preloaded_chunk_closes_connection", async () => {
55
+ const stream = await createRestStream(`${ORIGIN}/large`, 42);
56
+ await serverAssertConnectionsCount(1);
57
+
58
+ await expect(async () => {
59
+ for await (const chunk of stream) {
60
+ await serverAssertConnectionsCount(1);
61
+ expect(chunk).toEqual(stream.res.text);
62
+ throw Error("bailout");
63
+ }
64
+ }).rejects.toThrowError("bailout");
65
+
66
+ await serverAssertConnectionsCount(0);
67
+ });
@@ -0,0 +1,173 @@
1
+ import http from "http";
2
+ import type { Readable } from "stream";
3
+ import delay from "delay";
4
+ import range from "lodash/range";
5
+ import { Headers } from "node-fetch";
6
+ import { DEFAULT_OPTIONS, RestRequest, RestResponse, RestStream } from "..";
7
+ import type { RestFetchReaderOptions } from "../internal/RestFetchReader";
8
+ import RestFetchReader from "../internal/RestFetchReader";
9
+
10
+ // Two European Euro symbols in UTF-8.
11
+ export const UTF8_BUF = Buffer.from([0xe2, 0x82, 0xac, 0xe2, 0x82, 0xac]);
12
+
13
+ // Binary data, all possible bytes.
14
+ export const BINARY_BUF = Buffer.from(range(256));
15
+
16
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
17
+ export const server = http.createServer(async (req, res) => {
18
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
19
+
20
+ // In node-fetch 2.6.13+, they stopped sending "Connection: close" by default:
21
+ // https://github.com/node-fetch/node-fetch/pull/1736 - which is a good thing:
22
+ // we can now send multiple requests through the same connection sequentially
23
+ // (it seems to be broken before). Since RestFetchReader calls
24
+ // controller.abort() when the client wants to interrupt the request, the
25
+ // connection is physically closed only if there is currently an active
26
+ // request (e.g. "/slow" below), and it's not closed when the request is
27
+ // successfully processed (which is, again, a good thing). So to be able to
28
+ // use serverAssertConnectionsCount() helper, we force the server to close the
29
+ // connection after each request, so serverAssertConnectionsCount() can work.
30
+ res.setHeader("Connection", "close");
31
+
32
+ // Returns a small response.
33
+ if (req.url === "/small") {
34
+ res.writeHead(200);
35
+ return res.end("ok");
36
+ }
37
+
38
+ // Returns a large, but quick response.
39
+ if (req.url === "/large") {
40
+ res.writeHead(200);
41
+ await write(res, 1024);
42
+ await delay(100);
43
+ await write(res, 1024 * 100);
44
+ return res.end("x".repeat(1024 * 200));
45
+ }
46
+
47
+ // Returns a large response, does it slowly.
48
+ if (req.url === "/slow") {
49
+ res.writeHead(200);
50
+ for (let timeStart = Date.now(); Date.now() - timeStart < 20000; ) {
51
+ const error = await write(res, 1024);
52
+ if (error) {
53
+ break;
54
+ } else {
55
+ await delay(10);
56
+ }
57
+ }
58
+
59
+ return res.end();
60
+ }
61
+
62
+ // Returns slow response as UTF-8 bytes, one after another.
63
+ if (req.url === "/small_slow_utf8") {
64
+ res.writeHead(200);
65
+ for (const byte of UTF8_BUF) {
66
+ await write(res, Buffer.from([byte]));
67
+ await delay(500);
68
+ }
69
+
70
+ return res.end();
71
+ }
72
+
73
+ // Returns a binary response.
74
+ if (req.url === "/binary") {
75
+ res.setHeader("Content-Type", "application/octet-stream");
76
+ res.writeHead(200);
77
+ for (const _ in range(10)) {
78
+ await write(res, BINARY_BUF);
79
+ await delay(50);
80
+ }
81
+
82
+ return res.end();
83
+ }
84
+
85
+ res.writeHead(404);
86
+ return res.end("not found");
87
+ });
88
+
89
+ export async function serverAssertConnectionsCount(expect: number) {
90
+ let got: number = 0;
91
+ for (let timeStart = Date.now(); Date.now() - timeStart < 10000; ) {
92
+ got = await new Promise((resolve) =>
93
+ server.getConnections((_, count) => resolve(count)),
94
+ );
95
+ if (got === expect) {
96
+ return;
97
+ }
98
+
99
+ await delay(50);
100
+ }
101
+
102
+ throw Error(`Expected ${expect} connections, but got ${got}`);
103
+ }
104
+
105
+ export class TimeoutError extends Error {}
106
+
107
+ export function createFetchReader(
108
+ url: string,
109
+ options: RestFetchReaderOptions = {},
110
+ ) {
111
+ return new RestFetchReader(
112
+ url,
113
+ {},
114
+ {
115
+ onTimeout: () => {
116
+ throw new TimeoutError("timed out");
117
+ },
118
+ ...options,
119
+ },
120
+ );
121
+ }
122
+
123
+ export async function createRestStream(url: string, preloadBytes?: number) {
124
+ const reader = createFetchReader(url);
125
+ if (preloadBytes) {
126
+ await reader.preload(preloadBytes);
127
+ }
128
+
129
+ return new RestStream(
130
+ new RestResponse(
131
+ new RestRequest(DEFAULT_OPTIONS, "GET", "dummy", new Headers(), ""),
132
+ reader.agent,
133
+ reader.status,
134
+ reader.headers,
135
+ reader.textFetched.toString(),
136
+ reader.textIsPartial,
137
+ ),
138
+ reader[Symbol.asyncIterator](),
139
+ );
140
+ }
141
+
142
+ export async function consumeIterable(
143
+ iterable: AsyncIterable<string>,
144
+ delayMs?: number,
145
+ ) {
146
+ let rest = "";
147
+ for await (const chunk of iterable) {
148
+ rest += chunk;
149
+ if (delayMs) {
150
+ await delay(delayMs);
151
+ }
152
+ }
153
+
154
+ return rest;
155
+ }
156
+
157
+ export async function consumeReadable(stream: Readable) {
158
+ return new Promise<string>((resolve) => {
159
+ const chunks: string[] = [];
160
+ stream.on("data", (chunk) => {
161
+ chunks.push(chunk);
162
+ });
163
+ stream.on("end", () => {
164
+ resolve(chunks.join(""));
165
+ });
166
+ });
167
+ }
168
+
169
+ async function write(res: http.ServerResponse, bytes: number | Buffer) {
170
+ return new Promise((r) =>
171
+ res.write(typeof bytes === "number" ? "x".repeat(bytes) : bytes, r),
172
+ );
173
+ }
package/src/index.ts CHANGED
@@ -7,10 +7,7 @@ import RestRetriableError from "./errors/RestRetriableError";
7
7
  import RestTimeoutError from "./errors/RestTimeoutError";
8
8
  import RestTokenInvalidError from "./errors/RestTokenInvalidError";
9
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";
10
+ import paceRequests, { Pacer, PacerOutcome } from "./middlewares/paceRequests";
14
11
  import RestClient, { TokenGetter } from "./RestClient";
15
12
  import RestOptions, {
16
13
  RestLogEvent,
@@ -29,10 +26,8 @@ export {
29
26
  Headers,
30
27
  Middleware,
31
28
  Pacer,
32
- PacerComposite,
33
29
  paceRequests,
34
- PacerQPS,
35
- PacerQPSBackend,
30
+ PacerOutcome,
36
31
  RestClient,
37
32
  RestContentSizeOverLimitError,
38
33
  RestError,
@@ -10,6 +10,7 @@ export interface RestFetchReaderOptions {
10
10
  heartbeat?: () => Promise<void>;
11
11
  onTimeout?: (reader: RestFetchReader, e: any) => void;
12
12
  onAfterRead?: (reader: RestFetchReader) => void;
13
+ responseEncoding?: NodeJS.BufferEncoding;
13
14
  }
14
15
 
15
16
  /**
@@ -160,7 +161,9 @@ export default class RestFetchReader {
160
161
  // how Node streams and setEncoding() handle decoding when the returned
161
162
  // chunks cross the boundaries of multi-byte characters (TL;DR: it works
162
163
  // fine, that's why we work with string and not Buffer here).
163
- res.body.setEncoding(inferResBodyEncoding(res));
164
+ res.body.setEncoding(
165
+ this._options.responseEncoding ?? inferResBodyEncoding(res),
166
+ );
164
167
 
165
168
  await this._options.heartbeat?.();
166
169
  for await (const chunk of res.body) {
@@ -0,0 +1,16 @@
1
+ const ELLIPSIS = "…";
2
+
3
+ /**
4
+ * The fastest possible version of truncation. Lodash'es truncate() messes up
5
+ * with unicode a lot, so for e.g. logging purposes, it's super-slow.
6
+ */
7
+ export default function ellipsis(string: any, length: number) {
8
+ string = ("" + string).trimEnd();
9
+ length = Math.max(length, ELLIPSIS.length);
10
+
11
+ if (string.length <= length) {
12
+ return string;
13
+ }
14
+
15
+ return string.substring(0, length - ELLIPSIS.length) + ELLIPSIS;
16
+ }
@@ -1,6 +1,6 @@
1
1
  import { inspect } from "util";
2
2
  import sortBy from "lodash/sortBy";
3
- import truncate from "lodash/truncate";
3
+ import ellipsis from "./ellipsis";
4
4
 
5
5
  export default function inspectPossibleJSON(
6
6
  headers: { get(name: string): string | null },
@@ -65,7 +65,3 @@ function reorderObjectProps(
65
65
  Object.fromEntries(sortBy(entries, ([k, v]) => ranker(k, v))),
66
66
  );
67
67
  }
68
-
69
- function ellipsis(text: any, length: number) {
70
- return truncate("" + text, { length }).trimEnd();
71
- }
@@ -36,7 +36,7 @@ export default function throwIfErrorResponse(
36
36
  const retryAfterHeader = res.headers.get("Retry-After") || "0";
37
37
  throw new RestRateLimitError(
38
38
  `Rate limited by HTTP status ${STATUS_TOO_MANY_REQUESTS}`,
39
- parseInt(retryAfterHeader) || 0,
39
+ 1000 * parseInt(retryAfterHeader) || 0,
40
40
  res,
41
41
  );
42
42
  }