@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.
@@ -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
- const dispatcher = await (0, http_dispatcher_1.getDefaultDispatcher)();
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
- const nativeResponse = await fetch(url, nativeFetchOptions);
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 (_a) {
163
+ catch (_b) {
152
164
  responseData = responseText;
153
165
  }
154
166
  error.data = responseData;
155
167
  error.response.data = responseData;
156
168
  }
157
- catch (_b) {
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 (_c) {
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 defaultDispatcherPromise;
46
- function getDefaultDispatcher() {
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 (!defaultDispatcherPromise) {
51
- defaultDispatcherPromise = createDefaultDispatcher().catch((error) => {
52
- defaultDispatcherPromise = undefined;
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 defaultDispatcherPromise;
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 (!defaultDispatcherPromise) {
96
+ if (!defaultTransportPromise) {
60
97
  return;
61
98
  }
62
- const dispatcherPromise = defaultDispatcherPromise;
63
- defaultDispatcherPromise = undefined;
64
- await dispatcherPromise.then((dispatcher) => dispatcher.close(), () => undefined);
99
+ const transportPromise = defaultTransportPromise;
100
+ defaultTransport = undefined;
101
+ defaultTransportPromise = undefined;
102
+ await transportPromise.then((transport) => transport.dispatcher.close(), () => undefined);
65
103
  }
66
- async function createDefaultDispatcher() {
104
+ async function createDefaultTransport() {
67
105
  // Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
68
- // undici into their bundle. `getDefaultDispatcher` already short-circuits
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
- return agent;
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
- return agent.compose(decompress);
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 { getDefaultDispatcher } from './http-dispatcher.js';
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
- const dispatcher = await getDefaultDispatcher();
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
- const nativeResponse = await fetch(url, nativeFetchOptions);
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 (_a) {
160
+ catch (_b) {
149
161
  responseData = responseText;
150
162
  }
151
163
  error.data = responseData;
152
164
  error.response.data = responseData;
153
165
  }
154
- catch (_b) {
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 (_c) {
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 defaultDispatcherPromise;
8
- export function getDefaultDispatcher() {
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 (!defaultDispatcherPromise) {
13
- defaultDispatcherPromise = createDefaultDispatcher().catch((error) => {
14
- defaultDispatcherPromise = undefined;
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 defaultDispatcherPromise;
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 (!defaultDispatcherPromise) {
56
+ if (!defaultTransportPromise) {
22
57
  return;
23
58
  }
24
- const dispatcherPromise = defaultDispatcherPromise;
25
- defaultDispatcherPromise = undefined;
26
- await dispatcherPromise.then((dispatcher) => dispatcher.close(), () => undefined);
59
+ const transportPromise = defaultTransportPromise;
60
+ defaultTransport = undefined;
61
+ defaultTransportPromise = undefined;
62
+ await transportPromise.then((transport) => transport.dispatcher.close(), () => undefined);
27
63
  }
28
- async function createDefaultDispatcher() {
64
+ async function createDefaultTransport() {
29
65
  // Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
30
- // undici into their bundle. `getDefaultDispatcher` already short-circuits
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
- return agent;
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
- return agent.compose(decompress);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@doist/todoist-sdk",
3
- "version": "10.5.0",
3
+ "version": "10.5.1",
4
4
  "description": "The official TypeScript SDK for the Todoist REST API.",
5
5
  "author": "Doist developers",
6
6
  "repository": "https://github.com/Doist/todoist-sdk-typescript",