@doist/todoist-sdk 10.5.0 → 10.5.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.
- package/dist/cjs/test-utils/transport-seam.js +17 -0
- package/dist/cjs/transport/fetch-with-retry.js +19 -7
- package/dist/cjs/transport/http-dispatcher.js +60 -15
- package/dist/esm/test-utils/transport-seam.js +15 -0
- package/dist/esm/transport/fetch-with-retry.js +20 -8
- package/dist/esm/transport/http-dispatcher.js +58 -15
- package/dist/types/test-utils/transport-seam.d.ts +1 -0
- package/dist/types/transport/http-dispatcher.d.ts +36 -0
- package/package.json +1 -1
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
// In production the transport uses undici's own `fetch` (paired with the
|
|
5
|
+
// dispatcher) so the request client and dispatcher stay on one undici version.
|
|
6
|
+
// MSW intercepts the *global* `fetch`, not that separate undici instance, so
|
|
7
|
+
// force the transport onto the global `fetch` for every test in the suite; MSW
|
|
8
|
+
// then intercepts requests as usual. The real undici transport is covered
|
|
9
|
+
// directly by `http-dispatcher.test.ts` and `fetch-with-retry.test.ts`, which
|
|
10
|
+
// opt out of this seam with `vi.unmock('./http-dispatcher')`.
|
|
11
|
+
//
|
|
12
|
+
// This lives in its own setup file (not `msw-setup.ts`) because a hoisted
|
|
13
|
+
// `vi.mock` in a module that also re-exports values corrupts those exports.
|
|
14
|
+
vitest_1.vi.mock('../transport/http-dispatcher', async (importOriginal) => {
|
|
15
|
+
const actual = await importOriginal();
|
|
16
|
+
return Object.assign(Object.assign({}, actual), { getDefaultTransport: vitest_1.vi.fn(async () => undefined) });
|
|
17
|
+
});
|
|
@@ -99,6 +99,7 @@ function convertResponseToCustomFetch(response) {
|
|
|
99
99
|
* Performs a fetch request with retry logic and timeout support
|
|
100
100
|
*/
|
|
101
101
|
async function fetchWithRetry(args) {
|
|
102
|
+
var _a;
|
|
102
103
|
const { url, options = {}, retryConfig = {}, customFetch } = args;
|
|
103
104
|
const config = Object.assign(Object.assign({}, DEFAULT_RETRY_CONFIG), retryConfig);
|
|
104
105
|
const { timeout, signal: userSignal } = options, fetchOptions = __rest(options, ["timeout", "signal"]);
|
|
@@ -120,13 +121,24 @@ async function fetchWithRetry(args) {
|
|
|
120
121
|
fetchResponse = await customFetch(url, Object.assign(Object.assign({}, fetchOptions), { signal: requestSignal, timeout }));
|
|
121
122
|
}
|
|
122
123
|
else {
|
|
123
|
-
|
|
124
|
+
// Read the dispatcher and its paired `fetch` as one value so
|
|
125
|
+
// they can never be mismatched. On Node, `fetch` is undici's
|
|
126
|
+
// own — paired with the dispatcher — so the request client and
|
|
127
|
+
// dispatcher stay on one undici version; otherwise the
|
|
128
|
+
// `decompress` interceptor terminates gzip responses when the
|
|
129
|
+
// global `fetch`'s bundled undici differs. Browser/edge (and
|
|
130
|
+
// Bun) have no paired `fetch` and fall back to the global one.
|
|
131
|
+
const transport = await (0, http_dispatcher_1.getDefaultTransport)();
|
|
124
132
|
const nativeFetchOptions = Object.assign(Object.assign({}, fetchOptions), { signal: requestSignal });
|
|
125
|
-
if (dispatcher !== undefined) {
|
|
133
|
+
if ((transport === null || transport === void 0 ? void 0 : transport.dispatcher) !== undefined) {
|
|
126
134
|
// @ts-expect-error - dispatcher is a valid option for Node.js fetch but not in the TS types
|
|
127
|
-
nativeFetchOptions.dispatcher = dispatcher;
|
|
135
|
+
nativeFetchOptions.dispatcher = transport.dispatcher;
|
|
128
136
|
}
|
|
129
|
-
|
|
137
|
+
// undici's `fetch` and the global `fetch` are the same function
|
|
138
|
+
// at runtime but carry different (undici vs DOM) types. Call
|
|
139
|
+
// through the global signature, which matches the options above.
|
|
140
|
+
const fetchImpl = ((_a = transport === null || transport === void 0 ? void 0 : transport.fetch) !== null && _a !== void 0 ? _a : fetch);
|
|
141
|
+
const nativeResponse = await fetchImpl(url, nativeFetchOptions);
|
|
130
142
|
fetchResponse = convertResponseToCustomFetch(nativeResponse);
|
|
131
143
|
}
|
|
132
144
|
// Check if the response is successful
|
|
@@ -148,13 +160,13 @@ async function fetchWithRetry(args) {
|
|
|
148
160
|
try {
|
|
149
161
|
responseData = responseText ? JSON.parse(responseText) : undefined;
|
|
150
162
|
}
|
|
151
|
-
catch (
|
|
163
|
+
catch (_b) {
|
|
152
164
|
responseData = responseText;
|
|
153
165
|
}
|
|
154
166
|
error.data = responseData;
|
|
155
167
|
error.response.data = responseData;
|
|
156
168
|
}
|
|
157
|
-
catch (
|
|
169
|
+
catch (_c) {
|
|
158
170
|
// If we can't read the response body, that's OK
|
|
159
171
|
}
|
|
160
172
|
throw error;
|
|
@@ -165,7 +177,7 @@ async function fetchWithRetry(args) {
|
|
|
165
177
|
try {
|
|
166
178
|
data = responseText ? JSON.parse(responseText) : undefined;
|
|
167
179
|
}
|
|
168
|
-
catch (
|
|
180
|
+
catch (_d) {
|
|
169
181
|
// If JSON parsing fails, return the raw text
|
|
170
182
|
data = responseText;
|
|
171
183
|
}
|
|
@@ -33,7 +33,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.getDefaultTransport = getDefaultTransport;
|
|
36
37
|
exports.getDefaultDispatcher = getDefaultDispatcher;
|
|
38
|
+
exports.getDefaultFetch = getDefaultFetch;
|
|
37
39
|
exports.resetDefaultDispatcherForTests = resetDefaultDispatcherForTests;
|
|
38
40
|
exports.suppressExperimentalWarningsSync = suppressExperimentalWarningsSync;
|
|
39
41
|
// Use effectively-disabled keep-alive so short-lived CLI processes do not stay
|
|
@@ -42,32 +44,68 @@ const KEEP_ALIVE_OPTIONS = {
|
|
|
42
44
|
keepAliveTimeout: 1,
|
|
43
45
|
keepAliveMaxTimeout: 1,
|
|
44
46
|
};
|
|
45
|
-
let
|
|
46
|
-
|
|
47
|
+
let defaultTransport;
|
|
48
|
+
let defaultTransportPromise;
|
|
49
|
+
/**
|
|
50
|
+
* The default dispatcher and its paired `fetch`, as a single value so the two
|
|
51
|
+
* are always read consistently. Resolves to `undefined` outside Node (browser/
|
|
52
|
+
* edge), where callers use the global `fetch` with no dispatcher.
|
|
53
|
+
*/
|
|
54
|
+
function getDefaultTransport() {
|
|
47
55
|
if (!isNodeEnvironment()) {
|
|
48
56
|
return Promise.resolve(undefined);
|
|
49
57
|
}
|
|
50
|
-
if (!
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
if (!defaultTransportPromise) {
|
|
59
|
+
defaultTransportPromise = createDefaultTransport()
|
|
60
|
+
.then((transport) => {
|
|
61
|
+
defaultTransport = transport;
|
|
62
|
+
return transport;
|
|
63
|
+
})
|
|
64
|
+
.catch((error) => {
|
|
65
|
+
defaultTransport = undefined;
|
|
66
|
+
defaultTransportPromise = undefined;
|
|
53
67
|
throw error;
|
|
54
68
|
});
|
|
55
69
|
}
|
|
56
|
-
return
|
|
70
|
+
return defaultTransportPromise;
|
|
71
|
+
}
|
|
72
|
+
async function getDefaultDispatcher() {
|
|
73
|
+
var _a;
|
|
74
|
+
return (_a = (await getDefaultTransport())) === null || _a === void 0 ? void 0 : _a.dispatcher;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* The `fetch` implementation that must be used with the default dispatcher.
|
|
78
|
+
* Returns undici's own `fetch` on the full-undici Node path, or `undefined`
|
|
79
|
+
* (meaning: use the global `fetch`) in the browser/edge/Bun paths.
|
|
80
|
+
*
|
|
81
|
+
* Node's global `fetch` is backed by whatever undici version ships inside that
|
|
82
|
+
* Node release (6.x on Node 22 … 8.x on Node 26). Our dispatcher — and its
|
|
83
|
+
* `decompress` interceptor — comes from the npm `undici` package, which is a
|
|
84
|
+
* different version. Handing an npm-undici dispatcher to a mismatched built-in
|
|
85
|
+
* client makes gzip responses fail mid-stream with `terminated`. Sourcing
|
|
86
|
+
* `fetch` from the same npm `undici` keeps the whole request path on one
|
|
87
|
+
* version and removes the split.
|
|
88
|
+
*
|
|
89
|
+
* Only meaningful after {@link getDefaultTransport} has resolved, which is the
|
|
90
|
+
* one place that populates it.
|
|
91
|
+
*/
|
|
92
|
+
function getDefaultFetch() {
|
|
93
|
+
return defaultTransport === null || defaultTransport === void 0 ? void 0 : defaultTransport.fetch;
|
|
57
94
|
}
|
|
58
95
|
async function resetDefaultDispatcherForTests() {
|
|
59
|
-
if (!
|
|
96
|
+
if (!defaultTransportPromise) {
|
|
60
97
|
return;
|
|
61
98
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
99
|
+
const transportPromise = defaultTransportPromise;
|
|
100
|
+
defaultTransport = undefined;
|
|
101
|
+
defaultTransportPromise = undefined;
|
|
102
|
+
await transportPromise.then((transport) => transport.dispatcher.close(), () => undefined);
|
|
65
103
|
}
|
|
66
|
-
async function
|
|
104
|
+
async function createDefaultTransport() {
|
|
67
105
|
// Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
|
|
68
|
-
// undici into their bundle. `
|
|
106
|
+
// undici into their bundle. `getDefaultTransport` already short-circuits
|
|
69
107
|
// outside Node, so this branch only runs when undici is safe to load.
|
|
70
|
-
const { EnvHttpProxyAgent, interceptors } = await Promise.resolve().then(() => __importStar(require('undici')));
|
|
108
|
+
const { EnvHttpProxyAgent, interceptors, fetch: undiciFetch } = await Promise.resolve().then(() => __importStar(require('undici')));
|
|
71
109
|
const agent = new EnvHttpProxyAgent(KEEP_ALIVE_OPTIONS);
|
|
72
110
|
// Some runtimes report `process.versions.node` (so `isNodeEnvironment()`
|
|
73
111
|
// passes) but ship only a partial undici: `interceptors.decompress` is
|
|
@@ -76,7 +114,9 @@ async function createDefaultDispatcher() {
|
|
|
76
114
|
// gzip/deflate/br/zstd natively — so skip the interceptor instead of
|
|
77
115
|
// crashing on the missing API.
|
|
78
116
|
if (typeof interceptors.decompress !== 'function') {
|
|
79
|
-
|
|
117
|
+
// Bun: pair the agent with the global `fetch` (undefined), which
|
|
118
|
+
// decompresses natively.
|
|
119
|
+
return { dispatcher: agent, fetch: undefined };
|
|
80
120
|
}
|
|
81
121
|
// Compose the response-decompression interceptor so gzip/deflate/br/zstd
|
|
82
122
|
// bodies are decoded before consumers parse them. Required on Node 24+:
|
|
@@ -85,7 +125,12 @@ async function createDefaultDispatcher() {
|
|
|
85
125
|
// so callers receive raw gzipped bytes and `JSON.parse` fails.
|
|
86
126
|
// See https://github.com/Doist/todoist-cli/issues/318.
|
|
87
127
|
const decompress = suppressExperimentalWarningsSync(() => interceptors.decompress());
|
|
88
|
-
|
|
128
|
+
// Pair undici's own `fetch` with this dispatcher so the request client and
|
|
129
|
+
// the dispatcher stay on one undici version (see `getDefaultFetch`). The
|
|
130
|
+
// global `fetch` is backed by a different, Node-bundled undici; mixing the
|
|
131
|
+
// two makes the decompress interceptor terminate gzip responses on some
|
|
132
|
+
// Node versions.
|
|
133
|
+
return { dispatcher: agent.compose(decompress), fetch: undiciFetch };
|
|
89
134
|
}
|
|
90
135
|
// undici emits an `ExperimentalWarning` the first time `interceptors.decompress()`
|
|
91
136
|
// runs. The interceptor is stable for our gzipped-JSON-over-HTTPS use case;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
// In production the transport uses undici's own `fetch` (paired with the
|
|
3
|
+
// dispatcher) so the request client and dispatcher stay on one undici version.
|
|
4
|
+
// MSW intercepts the *global* `fetch`, not that separate undici instance, so
|
|
5
|
+
// force the transport onto the global `fetch` for every test in the suite; MSW
|
|
6
|
+
// then intercepts requests as usual. The real undici transport is covered
|
|
7
|
+
// directly by `http-dispatcher.test.ts` and `fetch-with-retry.test.ts`, which
|
|
8
|
+
// opt out of this seam with `vi.unmock('./http-dispatcher')`.
|
|
9
|
+
//
|
|
10
|
+
// This lives in its own setup file (not `msw-setup.ts`) because a hoisted
|
|
11
|
+
// `vi.mock` in a module that also re-exports values corrupts those exports.
|
|
12
|
+
vi.mock('../transport/http-dispatcher', async (importOriginal) => {
|
|
13
|
+
const actual = await importOriginal();
|
|
14
|
+
return Object.assign(Object.assign({}, actual), { getDefaultTransport: vi.fn(async () => undefined) });
|
|
15
|
+
});
|
|
@@ -10,7 +10,7 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
10
10
|
return t;
|
|
11
11
|
};
|
|
12
12
|
import { isNetworkError } from '../types/http.js';
|
|
13
|
-
import {
|
|
13
|
+
import { getDefaultTransport } from './http-dispatcher.js';
|
|
14
14
|
/**
|
|
15
15
|
* Default retry configuration matching the original axios-retry behavior
|
|
16
16
|
*/
|
|
@@ -96,6 +96,7 @@ function convertResponseToCustomFetch(response) {
|
|
|
96
96
|
* Performs a fetch request with retry logic and timeout support
|
|
97
97
|
*/
|
|
98
98
|
export async function fetchWithRetry(args) {
|
|
99
|
+
var _a;
|
|
99
100
|
const { url, options = {}, retryConfig = {}, customFetch } = args;
|
|
100
101
|
const config = Object.assign(Object.assign({}, DEFAULT_RETRY_CONFIG), retryConfig);
|
|
101
102
|
const { timeout, signal: userSignal } = options, fetchOptions = __rest(options, ["timeout", "signal"]);
|
|
@@ -117,13 +118,24 @@ export async function fetchWithRetry(args) {
|
|
|
117
118
|
fetchResponse = await customFetch(url, Object.assign(Object.assign({}, fetchOptions), { signal: requestSignal, timeout }));
|
|
118
119
|
}
|
|
119
120
|
else {
|
|
120
|
-
|
|
121
|
+
// Read the dispatcher and its paired `fetch` as one value so
|
|
122
|
+
// they can never be mismatched. On Node, `fetch` is undici's
|
|
123
|
+
// own — paired with the dispatcher — so the request client and
|
|
124
|
+
// dispatcher stay on one undici version; otherwise the
|
|
125
|
+
// `decompress` interceptor terminates gzip responses when the
|
|
126
|
+
// global `fetch`'s bundled undici differs. Browser/edge (and
|
|
127
|
+
// Bun) have no paired `fetch` and fall back to the global one.
|
|
128
|
+
const transport = await getDefaultTransport();
|
|
121
129
|
const nativeFetchOptions = Object.assign(Object.assign({}, fetchOptions), { signal: requestSignal });
|
|
122
|
-
if (dispatcher !== undefined) {
|
|
130
|
+
if ((transport === null || transport === void 0 ? void 0 : transport.dispatcher) !== undefined) {
|
|
123
131
|
// @ts-expect-error - dispatcher is a valid option for Node.js fetch but not in the TS types
|
|
124
|
-
nativeFetchOptions.dispatcher = dispatcher;
|
|
132
|
+
nativeFetchOptions.dispatcher = transport.dispatcher;
|
|
125
133
|
}
|
|
126
|
-
|
|
134
|
+
// undici's `fetch` and the global `fetch` are the same function
|
|
135
|
+
// at runtime but carry different (undici vs DOM) types. Call
|
|
136
|
+
// through the global signature, which matches the options above.
|
|
137
|
+
const fetchImpl = ((_a = transport === null || transport === void 0 ? void 0 : transport.fetch) !== null && _a !== void 0 ? _a : fetch);
|
|
138
|
+
const nativeResponse = await fetchImpl(url, nativeFetchOptions);
|
|
127
139
|
fetchResponse = convertResponseToCustomFetch(nativeResponse);
|
|
128
140
|
}
|
|
129
141
|
// Check if the response is successful
|
|
@@ -145,13 +157,13 @@ export async function fetchWithRetry(args) {
|
|
|
145
157
|
try {
|
|
146
158
|
responseData = responseText ? JSON.parse(responseText) : undefined;
|
|
147
159
|
}
|
|
148
|
-
catch (
|
|
160
|
+
catch (_b) {
|
|
149
161
|
responseData = responseText;
|
|
150
162
|
}
|
|
151
163
|
error.data = responseData;
|
|
152
164
|
error.response.data = responseData;
|
|
153
165
|
}
|
|
154
|
-
catch (
|
|
166
|
+
catch (_c) {
|
|
155
167
|
// If we can't read the response body, that's OK
|
|
156
168
|
}
|
|
157
169
|
throw error;
|
|
@@ -162,7 +174,7 @@ export async function fetchWithRetry(args) {
|
|
|
162
174
|
try {
|
|
163
175
|
data = responseText ? JSON.parse(responseText) : undefined;
|
|
164
176
|
}
|
|
165
|
-
catch (
|
|
177
|
+
catch (_d) {
|
|
166
178
|
// If JSON parsing fails, return the raw text
|
|
167
179
|
data = responseText;
|
|
168
180
|
}
|
|
@@ -4,32 +4,68 @@ const KEEP_ALIVE_OPTIONS = {
|
|
|
4
4
|
keepAliveTimeout: 1,
|
|
5
5
|
keepAliveMaxTimeout: 1,
|
|
6
6
|
};
|
|
7
|
-
let
|
|
8
|
-
|
|
7
|
+
let defaultTransport;
|
|
8
|
+
let defaultTransportPromise;
|
|
9
|
+
/**
|
|
10
|
+
* The default dispatcher and its paired `fetch`, as a single value so the two
|
|
11
|
+
* are always read consistently. Resolves to `undefined` outside Node (browser/
|
|
12
|
+
* edge), where callers use the global `fetch` with no dispatcher.
|
|
13
|
+
*/
|
|
14
|
+
export function getDefaultTransport() {
|
|
9
15
|
if (!isNodeEnvironment()) {
|
|
10
16
|
return Promise.resolve(undefined);
|
|
11
17
|
}
|
|
12
|
-
if (!
|
|
13
|
-
|
|
14
|
-
|
|
18
|
+
if (!defaultTransportPromise) {
|
|
19
|
+
defaultTransportPromise = createDefaultTransport()
|
|
20
|
+
.then((transport) => {
|
|
21
|
+
defaultTransport = transport;
|
|
22
|
+
return transport;
|
|
23
|
+
})
|
|
24
|
+
.catch((error) => {
|
|
25
|
+
defaultTransport = undefined;
|
|
26
|
+
defaultTransportPromise = undefined;
|
|
15
27
|
throw error;
|
|
16
28
|
});
|
|
17
29
|
}
|
|
18
|
-
return
|
|
30
|
+
return defaultTransportPromise;
|
|
31
|
+
}
|
|
32
|
+
export async function getDefaultDispatcher() {
|
|
33
|
+
var _a;
|
|
34
|
+
return (_a = (await getDefaultTransport())) === null || _a === void 0 ? void 0 : _a.dispatcher;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The `fetch` implementation that must be used with the default dispatcher.
|
|
38
|
+
* Returns undici's own `fetch` on the full-undici Node path, or `undefined`
|
|
39
|
+
* (meaning: use the global `fetch`) in the browser/edge/Bun paths.
|
|
40
|
+
*
|
|
41
|
+
* Node's global `fetch` is backed by whatever undici version ships inside that
|
|
42
|
+
* Node release (6.x on Node 22 … 8.x on Node 26). Our dispatcher — and its
|
|
43
|
+
* `decompress` interceptor — comes from the npm `undici` package, which is a
|
|
44
|
+
* different version. Handing an npm-undici dispatcher to a mismatched built-in
|
|
45
|
+
* client makes gzip responses fail mid-stream with `terminated`. Sourcing
|
|
46
|
+
* `fetch` from the same npm `undici` keeps the whole request path on one
|
|
47
|
+
* version and removes the split.
|
|
48
|
+
*
|
|
49
|
+
* Only meaningful after {@link getDefaultTransport} has resolved, which is the
|
|
50
|
+
* one place that populates it.
|
|
51
|
+
*/
|
|
52
|
+
export function getDefaultFetch() {
|
|
53
|
+
return defaultTransport === null || defaultTransport === void 0 ? void 0 : defaultTransport.fetch;
|
|
19
54
|
}
|
|
20
55
|
export async function resetDefaultDispatcherForTests() {
|
|
21
|
-
if (!
|
|
56
|
+
if (!defaultTransportPromise) {
|
|
22
57
|
return;
|
|
23
58
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
59
|
+
const transportPromise = defaultTransportPromise;
|
|
60
|
+
defaultTransport = undefined;
|
|
61
|
+
defaultTransportPromise = undefined;
|
|
62
|
+
await transportPromise.then((transport) => transport.dispatcher.close(), () => undefined);
|
|
27
63
|
}
|
|
28
|
-
async function
|
|
64
|
+
async function createDefaultTransport() {
|
|
29
65
|
// Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
|
|
30
|
-
// undici into their bundle. `
|
|
66
|
+
// undici into their bundle. `getDefaultTransport` already short-circuits
|
|
31
67
|
// outside Node, so this branch only runs when undici is safe to load.
|
|
32
|
-
const { EnvHttpProxyAgent, interceptors } = await import('undici');
|
|
68
|
+
const { EnvHttpProxyAgent, interceptors, fetch: undiciFetch } = await import('undici');
|
|
33
69
|
const agent = new EnvHttpProxyAgent(KEEP_ALIVE_OPTIONS);
|
|
34
70
|
// Some runtimes report `process.versions.node` (so `isNodeEnvironment()`
|
|
35
71
|
// passes) but ship only a partial undici: `interceptors.decompress` is
|
|
@@ -38,7 +74,9 @@ async function createDefaultDispatcher() {
|
|
|
38
74
|
// gzip/deflate/br/zstd natively — so skip the interceptor instead of
|
|
39
75
|
// crashing on the missing API.
|
|
40
76
|
if (typeof interceptors.decompress !== 'function') {
|
|
41
|
-
|
|
77
|
+
// Bun: pair the agent with the global `fetch` (undefined), which
|
|
78
|
+
// decompresses natively.
|
|
79
|
+
return { dispatcher: agent, fetch: undefined };
|
|
42
80
|
}
|
|
43
81
|
// Compose the response-decompression interceptor so gzip/deflate/br/zstd
|
|
44
82
|
// bodies are decoded before consumers parse them. Required on Node 24+:
|
|
@@ -47,7 +85,12 @@ async function createDefaultDispatcher() {
|
|
|
47
85
|
// so callers receive raw gzipped bytes and `JSON.parse` fails.
|
|
48
86
|
// See https://github.com/Doist/todoist-cli/issues/318.
|
|
49
87
|
const decompress = suppressExperimentalWarningsSync(() => interceptors.decompress());
|
|
50
|
-
|
|
88
|
+
// Pair undici's own `fetch` with this dispatcher so the request client and
|
|
89
|
+
// the dispatcher stay on one undici version (see `getDefaultFetch`). The
|
|
90
|
+
// global `fetch` is backed by a different, Node-bundled undici; mixing the
|
|
91
|
+
// two makes the decompress interceptor terminate gzip responses on some
|
|
92
|
+
// Node versions.
|
|
93
|
+
return { dispatcher: agent.compose(decompress), fetch: undiciFetch };
|
|
51
94
|
}
|
|
52
95
|
// undici emits an `ExperimentalWarning` the first time `interceptors.decompress()`
|
|
53
96
|
// runs. The interceptor is stable for our gzipped-JSON-over-HTTPS use case;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
import type { Dispatcher } from 'undici';
|
|
2
|
+
type UndiciFetch = typeof import('undici').fetch;
|
|
3
|
+
/**
|
|
4
|
+
* A dispatcher and the `fetch` that must be used with it. `fetch` is undici's
|
|
5
|
+
* own `fetch` on the full-undici Node path, or `undefined` (meaning: use the
|
|
6
|
+
* global `fetch`) on the Bun path. The two are cached together so callers can
|
|
7
|
+
* never observe a dispatcher paired with a mismatched `fetch` — see
|
|
8
|
+
* {@link getDefaultFetch} for why the pairing matters.
|
|
9
|
+
*/
|
|
10
|
+
type DefaultTransport = {
|
|
11
|
+
dispatcher: Dispatcher;
|
|
12
|
+
fetch: UndiciFetch | undefined;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* The default dispatcher and its paired `fetch`, as a single value so the two
|
|
16
|
+
* are always read consistently. Resolves to `undefined` outside Node (browser/
|
|
17
|
+
* edge), where callers use the global `fetch` with no dispatcher.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getDefaultTransport(): Promise<DefaultTransport | undefined>;
|
|
2
20
|
export declare function getDefaultDispatcher(): Promise<Dispatcher | undefined>;
|
|
21
|
+
/**
|
|
22
|
+
* The `fetch` implementation that must be used with the default dispatcher.
|
|
23
|
+
* Returns undici's own `fetch` on the full-undici Node path, or `undefined`
|
|
24
|
+
* (meaning: use the global `fetch`) in the browser/edge/Bun paths.
|
|
25
|
+
*
|
|
26
|
+
* Node's global `fetch` is backed by whatever undici version ships inside that
|
|
27
|
+
* Node release (6.x on Node 22 … 8.x on Node 26). Our dispatcher — and its
|
|
28
|
+
* `decompress` interceptor — comes from the npm `undici` package, which is a
|
|
29
|
+
* different version. Handing an npm-undici dispatcher to a mismatched built-in
|
|
30
|
+
* client makes gzip responses fail mid-stream with `terminated`. Sourcing
|
|
31
|
+
* `fetch` from the same npm `undici` keeps the whole request path on one
|
|
32
|
+
* version and removes the split.
|
|
33
|
+
*
|
|
34
|
+
* Only meaningful after {@link getDefaultTransport} has resolved, which is the
|
|
35
|
+
* one place that populates it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getDefaultFetch(): UndiciFetch | undefined;
|
|
3
38
|
export declare function resetDefaultDispatcherForTests(): Promise<void>;
|
|
4
39
|
export declare function suppressExperimentalWarningsSync<T>(fn: () => T): T;
|
|
40
|
+
export {};
|
package/package.json
CHANGED