@doist/comms-sdk 2.0.1 → 3.0.0

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 (48) hide show
  1. package/dist/esm/utils/multipart-upload.js +73 -15
  2. package/dist/esm/utils/url-helpers.js +104 -16
  3. package/dist/types/utils/multipart-upload.d.ts +3 -4
  4. package/dist/types/utils/url-helpers.d.ts +41 -0
  5. package/package.json +5 -10
  6. package/dist/cjs/authentication.js +0 -256
  7. package/dist/cjs/clients/add-comment-helper.js +0 -43
  8. package/dist/cjs/clients/attachments-client.js +0 -71
  9. package/dist/cjs/clients/base-client.js +0 -33
  10. package/dist/cjs/clients/channels-client.js +0 -211
  11. package/dist/cjs/clients/comments-client.js +0 -175
  12. package/dist/cjs/clients/conversation-messages-client.js +0 -169
  13. package/dist/cjs/clients/conversations-client.js +0 -283
  14. package/dist/cjs/clients/groups-client.js +0 -172
  15. package/dist/cjs/clients/hooks-client.js +0 -52
  16. package/dist/cjs/clients/inbox-client.js +0 -184
  17. package/dist/cjs/clients/notify-audience.js +0 -45
  18. package/dist/cjs/clients/reactions-client.js +0 -97
  19. package/dist/cjs/clients/search-client.js +0 -138
  20. package/dist/cjs/clients/threads-client.js +0 -364
  21. package/dist/cjs/clients/users-client.js +0 -326
  22. package/dist/cjs/clients/workspace-users-client.js +0 -252
  23. package/dist/cjs/clients/workspaces-client.js +0 -174
  24. package/dist/cjs/comms-api.js +0 -70
  25. package/dist/cjs/consts/endpoints.js +0 -34
  26. package/dist/cjs/index.js +0 -53
  27. package/dist/cjs/migration.js +0 -96
  28. package/dist/cjs/package.json +0 -1
  29. package/dist/cjs/testUtils/msw-handlers.js +0 -51
  30. package/dist/cjs/testUtils/msw-setup.js +0 -35
  31. package/dist/cjs/testUtils/obsidian-fetch-adapter.js +0 -53
  32. package/dist/cjs/testUtils/test-defaults.js +0 -106
  33. package/dist/cjs/transport/fetch-with-retry.js +0 -146
  34. package/dist/cjs/transport/http-client.js +0 -56
  35. package/dist/cjs/transport/http-dispatcher.js +0 -190
  36. package/dist/cjs/types/api-version.js +0 -8
  37. package/dist/cjs/types/entities.js +0 -496
  38. package/dist/cjs/types/enums.js +0 -37
  39. package/dist/cjs/types/errors.js +0 -12
  40. package/dist/cjs/types/http.js +0 -4
  41. package/dist/cjs/types/index.js +0 -22
  42. package/dist/cjs/types/requests.js +0 -183
  43. package/dist/cjs/utils/case-conversion.js +0 -54
  44. package/dist/cjs/utils/index.js +0 -19
  45. package/dist/cjs/utils/multipart-upload.js +0 -144
  46. package/dist/cjs/utils/timestamp-conversion.js +0 -58
  47. package/dist/cjs/utils/url-helpers.js +0 -133
  48. package/dist/cjs/utils/uuidv7.js +0 -182
@@ -1,51 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.HttpResponse = exports.http = void 0;
4
- exports.createSuccessResponse = createSuccessResponse;
5
- exports.createErrorResponse = createErrorResponse;
6
- exports.apiUrl = apiUrl;
7
- exports.createGetHandler = createGetHandler;
8
- exports.createPostHandler = createPostHandler;
9
- const msw_1 = require("msw");
10
- Object.defineProperty(exports, "HttpResponse", { enumerable: true, get: function () { return msw_1.HttpResponse; } });
11
- Object.defineProperty(exports, "http", { enumerable: true, get: function () { return msw_1.http; } });
12
- const BASE_URL = 'https://comms.todoist.com';
13
- /**
14
- * Creates a successful API response with the given data
15
- */
16
- function createSuccessResponse(data) {
17
- return msw_1.HttpResponse.json(data, { status: 200 });
18
- }
19
- /**
20
- * Creates an error API response
21
- */
22
- function createErrorResponse(errorCode, errorMessage, status = 400) {
23
- return msw_1.HttpResponse.json({
24
- error_code: errorCode,
25
- error_string: errorMessage,
26
- }, { status });
27
- }
28
- /**
29
- * Helper to create API endpoint URL
30
- */
31
- function apiUrl(path) {
32
- // Remove leading slash if present
33
- const cleanPath = path.startsWith('/') ? path.slice(1) : path;
34
- return `${BASE_URL}/${cleanPath}`;
35
- }
36
- /**
37
- * Creates a handler for a GET endpoint
38
- */
39
- function createGetHandler(endpoint, responseData) {
40
- return msw_1.http.get(apiUrl(endpoint), () => {
41
- return createSuccessResponse(responseData);
42
- });
43
- }
44
- /**
45
- * Creates a handler for a POST endpoint
46
- */
47
- function createPostHandler(endpoint, responseData) {
48
- return msw_1.http.post(apiUrl(endpoint), () => {
49
- return createSuccessResponse(responseData);
50
- });
51
- }
@@ -1,35 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.server = void 0;
4
- const node_1 = require("msw/node");
5
- const vitest_1 = require("vitest");
6
- // In production the transport uses undici's own `fetch` (paired with the
7
- // dispatcher) so the request client and dispatcher stay on one undici version.
8
- // MSW intercepts the *global* `fetch`, not that separate undici instance, so
9
- // force the transport onto the global `fetch` for every test in the suite;
10
- // MSW then intercepts requests as usual. The real undici transport is covered
11
- // directly by `http-dispatcher.test.ts` and `fetch-with-retry.test.ts`, which
12
- // opt out of this seam.
13
- vitest_1.vi.mock('../transport/http-dispatcher', async (importOriginal) => {
14
- const actual = await importOriginal();
15
- return {
16
- ...actual,
17
- getDefaultTransport: vitest_1.vi.fn(async () => undefined),
18
- };
19
- });
20
- // Create MSW server instance
21
- exports.server = (0, node_1.setupServer)();
22
- // Start server before all tests
23
- (0, vitest_1.beforeAll)(() => {
24
- // Only warn on unhandled requests instead of error, since transport/http-client.test.ts
25
- // uses direct fetch mocking which bypasses MSW
26
- exports.server.listen({ onUnhandledRequest: 'warn' });
27
- });
28
- // Reset handlers after each test
29
- (0, vitest_1.afterEach)(() => {
30
- exports.server.resetHandlers();
31
- });
32
- // Clean up after all tests
33
- (0, vitest_1.afterAll)(() => {
34
- exports.server.close();
35
- });
@@ -1,53 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createObsidianFetchAdapter = createObsidianFetchAdapter;
4
- /**
5
- * Creates a CustomFetch adapter for Obsidian's requestUrl API.
6
- *
7
- * This adapter bridges the gap between Obsidian's requestUrl interface and the
8
- * standard fetch-like interface expected by the Comms API SDK.
9
- *
10
- * Key differences handled by this adapter:
11
- * - Obsidian returns response data as properties (response.json, response.text)
12
- * while the SDK expects methods (response.json(), response.text())
13
- * - Obsidian's requestUrl bypasses CORS restrictions that would block standard fetch
14
- * - Obsidian throws on HTTP errors by default; we set throw: false to handle manually
15
- * - Obsidian doesn't provide statusText; we default to empty string
16
- *
17
- * @example
18
- * ```typescript
19
- * import { requestUrl } from 'obsidian'
20
- * import { createObsidianFetchAdapter } from './obsidian-fetch-adapter'
21
- *
22
- * const api = new CommsApi('your-token', {
23
- * customFetch: createObsidianFetchAdapter(requestUrl)
24
- * })
25
- * ```
26
- *
27
- * @param requestUrl - The Obsidian requestUrl function
28
- * @returns A CustomFetch function compatible with the Comms API SDK
29
- */
30
- function createObsidianFetchAdapter(requestUrl) {
31
- return async (url, options) => {
32
- // Build the request parameters in Obsidian's format
33
- const requestParams = {
34
- url,
35
- method: options?.method || 'GET',
36
- headers: options?.headers,
37
- body: options?.body,
38
- throw: false, // Don't throw on HTTP errors; let the SDK handle status codes
39
- };
40
- // Make the request using Obsidian's requestUrl
41
- const response = await requestUrl(requestParams);
42
- // Transform Obsidian's response format to match CustomFetchResponse interface
43
- return {
44
- ok: response.status >= 200 && response.status < 300,
45
- status: response.status,
46
- statusText: '', // Obsidian doesn't provide statusText
47
- headers: response.headers,
48
- // Wrap Obsidian's direct properties as methods returning promises
49
- text: () => Promise.resolve(response.text),
50
- json: () => Promise.resolve(response.json),
51
- };
52
- };
53
- }
@@ -1,106 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.mockWorkspaceUser = exports.mockComment = exports.mockConversation = exports.mockGroup = exports.mockThread = exports.mockChannel = exports.mockWorkspace = exports.mockUser = exports.TEST_GROUP_ID = exports.TEST_MESSAGE_ID = exports.TEST_CONVERSATION_ID = exports.TEST_COMMENT_ID = exports.TEST_THREAD_ID = exports.TEST_CHANNEL_ID = exports.TEST_API_BASE_URL = exports.TEST_API_TOKEN = void 0;
4
- const endpoints_1 = require("../consts/endpoints");
5
- exports.TEST_API_TOKEN = 'test-api-token';
6
- exports.TEST_API_BASE_URL = (0, endpoints_1.getCommsBaseUri)().replace(/\/$/, '');
7
- // Stable test IDs. TEST_CHANNEL_ID and TEST_THREAD_ID are base58
8
- // UUIDv7-shaped because create-thread and add-comment paths validate them
9
- // SDK-side.
10
- exports.TEST_CHANNEL_ID = 'CQ7yksHFbYFrSPVTaHVCs';
11
- exports.TEST_THREAD_ID = 'CQ7yksHFbYFrSPVTaHVCt';
12
- exports.TEST_COMMENT_ID = '7YpL3oZ4kZ9vP7Q1tR2sX41';
13
- exports.TEST_CONVERSATION_ID = '7YpL3oZ4kZ9vP7Q1tR2sX42';
14
- exports.TEST_MESSAGE_ID = '7YpL3oZ4kZ9vP7Q1tR2sX43';
15
- exports.TEST_GROUP_ID = '7YpL3oZ4kZ9vP7Q1tR2sX44';
16
- exports.mockUser = {
17
- id: 1,
18
- email: 'test@example.com',
19
- fullName: 'Test User',
20
- shortName: 'TU',
21
- timezone: 'America/New_York',
22
- removed: false,
23
- lang: 'en',
24
- };
25
- exports.mockWorkspace = {
26
- id: 1,
27
- name: 'Test Workspace',
28
- creator: 1,
29
- created: new Date('2021-01-01T00:00:00Z'),
30
- };
31
- exports.mockChannel = {
32
- id: exports.TEST_CHANNEL_ID,
33
- name: 'general',
34
- creator: 1,
35
- public: true,
36
- workspaceId: 1,
37
- archived: false,
38
- created: new Date('2021-01-01T00:00:00Z'),
39
- version: 0,
40
- url: `https://comms.todoist.com/1/ch/${exports.TEST_CHANNEL_ID}/`,
41
- };
42
- exports.mockThread = {
43
- id: exports.TEST_THREAD_ID,
44
- title: 'Test Thread',
45
- content: 'This is a test thread',
46
- creator: 1,
47
- channelId: exports.TEST_CHANNEL_ID,
48
- workspaceId: 1,
49
- commentCount: 0,
50
- lastUpdated: new Date('2021-01-01T00:00:00Z'),
51
- pinned: false,
52
- posted: new Date('2021-01-01T00:00:00Z'),
53
- snippet: 'This is a test thread',
54
- snippetCreator: 1,
55
- isArchived: false,
56
- url: `https://comms.todoist.com/1/ch/${exports.TEST_CHANNEL_ID}/t/${exports.TEST_THREAD_ID}/`,
57
- };
58
- exports.mockGroup = {
59
- id: exports.TEST_GROUP_ID,
60
- name: 'Test Group',
61
- workspaceId: 1,
62
- userIds: [1, 2, 3],
63
- version: 0,
64
- };
65
- exports.mockConversation = {
66
- id: exports.TEST_CONVERSATION_ID,
67
- workspaceId: 1,
68
- userIds: [1, 2],
69
- messageCount: 1,
70
- lastObjIndex: 0,
71
- snippet: 'Hello there',
72
- snippetCreators: [1],
73
- lastActive: new Date('2021-01-01T00:00:00Z'),
74
- archived: false,
75
- created: new Date('2021-01-01T00:00:00Z'),
76
- creator: 1,
77
- url: `https://comms.todoist.com/1/msg/${exports.TEST_CONVERSATION_ID}/`,
78
- };
79
- exports.mockComment = {
80
- id: exports.TEST_COMMENT_ID,
81
- content: 'This is a comment',
82
- creator: 1,
83
- threadId: exports.TEST_THREAD_ID,
84
- workspaceId: 1,
85
- channelId: exports.TEST_CHANNEL_ID,
86
- posted: new Date('2021-01-01T00:00:00Z'),
87
- url: `https://comms.todoist.com/1/ch/${exports.TEST_CHANNEL_ID}/t/${exports.TEST_THREAD_ID}/c/${exports.TEST_COMMENT_ID}`,
88
- };
89
- exports.mockWorkspaceUser = {
90
- id: 1,
91
- fullName: 'Test User',
92
- email: 'test@example.com',
93
- userType: 'USER',
94
- shortName: 'TU',
95
- firstName: 'Test',
96
- imageId: null,
97
- avatarUrls: null,
98
- dateFormat: null,
99
- removed: false,
100
- restricted: null,
101
- setupPending: null,
102
- theme: null,
103
- timeFormat: null,
104
- timezone: 'America/New_York',
105
- version: 1,
106
- };
@@ -1,146 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fetchWithRetry = fetchWithRetry;
4
- const errors_1 = require("../types/errors");
5
- const case_conversion_1 = require("../utils/case-conversion");
6
- const timestamp_conversion_1 = require("../utils/timestamp-conversion");
7
- const http_dispatcher_1 = require("./http-dispatcher");
8
- async function fetchWithRetry(url, options, maxRetries = 3, customFetch) {
9
- let lastError;
10
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
11
- let clearTimeoutFn;
12
- try {
13
- let requestSignal = options.signal || undefined;
14
- if (options.timeout && options.timeout > 0) {
15
- const timeoutResult = createTimeoutSignal(options.timeout, requestSignal);
16
- requestSignal = timeoutResult.signal;
17
- clearTimeoutFn = timeoutResult.clear;
18
- }
19
- const response = customFetch
20
- ? await customFetch(url, options)
21
- : await fetchWithDefaultTransport(url, options, requestSignal);
22
- const responseText = await response.text();
23
- let responseData;
24
- try {
25
- responseData = responseText ? JSON.parse(responseText) : undefined;
26
- }
27
- catch {
28
- responseData = responseText;
29
- }
30
- if (!response.ok) {
31
- throw new errors_1.CommsRequestError(`Request failed with status ${response.status}`, response.status, responseData);
32
- }
33
- const camelCased = (0, case_conversion_1.camelCaseKeys)(responseData);
34
- const transformed = (0, timestamp_conversion_1.transformTimestamps)(camelCased);
35
- if (clearTimeoutFn) {
36
- clearTimeoutFn();
37
- }
38
- return {
39
- data: transformed,
40
- status: response.status,
41
- headers: response.headers,
42
- };
43
- }
44
- catch (error) {
45
- if (clearTimeoutFn) {
46
- clearTimeoutFn();
47
- }
48
- lastError = error instanceof Error ? error : new Error('Unknown error');
49
- if (attempt < maxRetries && isNetworkError(lastError)) {
50
- const delay = getRetryDelay(attempt + 1);
51
- if (delay > 0) {
52
- await sleep(delay);
53
- }
54
- continue;
55
- }
56
- break;
57
- }
58
- }
59
- if (lastError instanceof errors_1.CommsRequestError) {
60
- throw lastError;
61
- }
62
- throw new errors_1.CommsRequestError(lastError?.message ?? 'Request failed');
63
- }
64
- async function fetchWithDefaultTransport(url, options, signal) {
65
- // Read the dispatcher and its paired `fetch` as one value so they can never
66
- // be mismatched. On Node, `fetch` is undici's own — paired with the
67
- // dispatcher — so the request client and dispatcher stay on one undici
68
- // version; otherwise the `decompress` interceptor terminates gzip responses
69
- // when the global `fetch`'s bundled undici differs. Browser/edge (and Bun)
70
- // have no paired `fetch` and fall back to the global one.
71
- const transport = await (0, http_dispatcher_1.getDefaultTransport)();
72
- // undici's `fetch` and the global `fetch` are the same function at runtime
73
- // but carry different (undici vs DOM) `RequestInit`/`Response` types. Call
74
- // through the global signature, which matches the global-typed `options`.
75
- const fetchImpl = (transport?.fetch ?? fetch);
76
- const response = transport?.dispatcher
77
- ? await fetchImpl(url, {
78
- ...options,
79
- signal,
80
- // @ts-expect-error - dispatcher is valid for Node.js fetch but not in TS types
81
- dispatcher: transport.dispatcher,
82
- })
83
- : await fetchImpl(url, {
84
- ...options,
85
- signal,
86
- });
87
- return convertResponseToCustomFetch(response);
88
- }
89
- async function sleep(ms) {
90
- return new Promise((resolve) => setTimeout(resolve, ms));
91
- }
92
- const timeoutErrorName = 'TimeoutError';
93
- function createTimeoutError(timeoutMs) {
94
- const error = new Error(`Request timeout after ${timeoutMs}ms`);
95
- error.name = timeoutErrorName;
96
- return error;
97
- }
98
- function isNetworkError(error) {
99
- return (error.name === 'TypeError' ||
100
- error.name === timeoutErrorName ||
101
- error.message.toLowerCase().includes('network'));
102
- }
103
- function getRetryDelay(retryCount) {
104
- return retryCount === 1 ? 0 : 500;
105
- }
106
- function createTimeoutSignal(timeoutMs, existingSignal) {
107
- const controller = new AbortController();
108
- const timeoutId = setTimeout(() => {
109
- controller.abort(createTimeoutError(timeoutMs));
110
- }, timeoutMs);
111
- let abortHandler;
112
- function clear() {
113
- clearTimeout(timeoutId);
114
- if (existingSignal && abortHandler) {
115
- existingSignal.removeEventListener('abort', abortHandler);
116
- }
117
- }
118
- if (existingSignal) {
119
- if (existingSignal.aborted) {
120
- clearTimeout(timeoutId);
121
- controller.abort(existingSignal.reason);
122
- }
123
- else {
124
- abortHandler = () => {
125
- clearTimeout(timeoutId);
126
- controller.abort(existingSignal.reason);
127
- };
128
- existingSignal.addEventListener('abort', abortHandler, { once: true });
129
- }
130
- }
131
- return { signal: controller.signal, clear };
132
- }
133
- function convertResponseToCustomFetch(response) {
134
- const headers = {};
135
- response.headers.forEach((value, key) => {
136
- headers[key] = value;
137
- });
138
- return {
139
- ok: response.ok,
140
- status: response.status,
141
- statusText: response.statusText,
142
- headers,
143
- text: () => response.clone().text(),
144
- json: () => response.json(),
145
- };
146
- }
@@ -1,56 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.paramsSerializer = paramsSerializer;
4
- exports.request = request;
5
- exports.isSuccess = isSuccess;
6
- const case_conversion_1 = require("../utils/case-conversion");
7
- const fetch_with_retry_1 = require("./fetch-with-retry");
8
- function paramsSerializer(params) {
9
- const qs = new URLSearchParams();
10
- Object.keys(params).forEach((key) => {
11
- const value = params[key];
12
- if (value != null) {
13
- if (Array.isArray(value)) {
14
- qs.append(key, value.join(','));
15
- }
16
- else {
17
- qs.append(key, String(value));
18
- }
19
- }
20
- });
21
- return qs.toString();
22
- }
23
- const defaultHeaders = {
24
- 'Content-Type': 'application/json',
25
- };
26
- function getAuthHeader(apiToken) {
27
- return `Bearer ${apiToken}`;
28
- }
29
- function getRequestConfiguration(baseURL, apiToken, requestId) {
30
- const authHeader = apiToken ? { Authorization: getAuthHeader(apiToken) } : undefined;
31
- const requestIdHeader = requestId ? { 'X-Request-Id': requestId } : undefined;
32
- const headers = { ...defaultHeaders, ...authHeader, ...requestIdHeader };
33
- return { baseURL, headers, timeout: 30000 };
34
- }
35
- async function request(args) {
36
- const { httpMethod, baseUri, relativePath, apiToken, payload, requestId, customFetch } = args;
37
- const config = getRequestConfiguration(baseUri, apiToken, requestId);
38
- const url = new URL(relativePath, config.baseURL).toString();
39
- const options = {
40
- method: httpMethod,
41
- headers: config.headers,
42
- timeout: config.timeout,
43
- };
44
- if (httpMethod === 'GET' && payload) {
45
- const searchParams = paramsSerializer((0, case_conversion_1.snakeCaseKeys)(payload));
46
- const urlWithParams = searchParams ? `${url}?${searchParams}` : url;
47
- return (0, fetch_with_retry_1.fetchWithRetry)(urlWithParams, options, 3, customFetch);
48
- }
49
- if (payload && httpMethod !== 'GET') {
50
- options.body = JSON.stringify((0, case_conversion_1.snakeCaseKeys)(payload));
51
- }
52
- return (0, fetch_with_retry_1.fetchWithRetry)(url, options, 3, customFetch);
53
- }
54
- function isSuccess(response) {
55
- return response.status >= 200 && response.status < 300;
56
- }
@@ -1,190 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.getDefaultTransport = getDefaultTransport;
37
- exports.getDefaultDispatcher = getDefaultDispatcher;
38
- exports.closeDefaultDispatcher = closeDefaultDispatcher;
39
- exports.resetDefaultDispatcherForTests = resetDefaultDispatcherForTests;
40
- exports.getDefaultFetch = getDefaultFetch;
41
- exports.suppressExperimentalWarningsSync = suppressExperimentalWarningsSync;
42
- let defaultTransport;
43
- let defaultTransportPromise;
44
- /**
45
- * The default dispatcher and its paired `fetch`, as a single value so the two
46
- * are always read consistently. Resolves to `undefined` outside Node (browser/
47
- * edge), where callers use the global `fetch` with no dispatcher.
48
- */
49
- async function getDefaultTransport() {
50
- if (defaultTransport) {
51
- return defaultTransport;
52
- }
53
- if (!defaultTransportPromise) {
54
- defaultTransportPromise = createDefaultTransport()
55
- .then((transport) => {
56
- defaultTransport = transport;
57
- return transport;
58
- })
59
- .catch((error) => {
60
- defaultTransport = undefined;
61
- defaultTransportPromise = undefined;
62
- throw error;
63
- });
64
- }
65
- return defaultTransportPromise;
66
- }
67
- async function getDefaultDispatcher() {
68
- return (await getDefaultTransport())?.dispatcher;
69
- }
70
- /**
71
- * Drains the default dispatcher's connection pool. CLIs and scripts should
72
- * `await` this before exit so Node's event loop empties immediately instead
73
- * of waiting ~4s on keep-alive. No-op in the browser branch.
74
- */
75
- async function closeDefaultDispatcher() {
76
- // Clear the singleton *before* awaiting init, so any concurrent
77
- // `getDefaultTransport()` after this point creates a fresh dispatcher
78
- // instead of receiving a reference to the one we're about to close.
79
- const initPromise = defaultTransportPromise;
80
- defaultTransport = undefined;
81
- defaultTransportPromise = undefined;
82
- if (!initPromise)
83
- return;
84
- try {
85
- const transport = await initPromise;
86
- if (transport) {
87
- await transport.dispatcher.close();
88
- }
89
- }
90
- catch {
91
- // init failed; nothing to close
92
- }
93
- }
94
- function resetDefaultDispatcherForTests() {
95
- defaultTransport = undefined;
96
- defaultTransportPromise = undefined;
97
- }
98
- /**
99
- * The `fetch` implementation that must be used with the default dispatcher.
100
- * Returns undici's own `fetch` on the full-undici Node path, or `undefined`
101
- * (meaning: use the global `fetch`) in the browser/edge/Bun paths.
102
- *
103
- * Node's global `fetch` is backed by whatever undici version ships inside that
104
- * Node release (6.x on Node 22 … 8.x on Node 26). Our dispatcher — and its
105
- * `decompress` interceptor — comes from the npm `undici` package, which is a
106
- * different version. Handing an npm-undici dispatcher to a mismatched built-in
107
- * client makes gzip responses fail mid-stream with `terminated`. Sourcing
108
- * `fetch` from the same npm `undici` keeps the whole request path on one
109
- * version and removes the split.
110
- *
111
- * Only meaningful after {@link getDefaultTransport} has resolved, which is the
112
- * one place that populates it.
113
- */
114
- function getDefaultFetch() {
115
- return defaultTransport?.fetch;
116
- }
117
- function isNodeEnvironment() {
118
- return typeof process !== 'undefined' && typeof process.versions?.node === 'string';
119
- }
120
- async function createDefaultTransport() {
121
- if (!isNodeEnvironment()) {
122
- return undefined;
123
- }
124
- // Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
125
- // undici into their bundle. `isNodeEnvironment()` above already gated this
126
- // branch, so undici is safe to load when we get here.
127
- const { EnvHttpProxyAgent, interceptors, fetch: undiciFetch } = await Promise.resolve().then(() => __importStar(require('undici')));
128
- // `EnvHttpProxyAgent` (with `allowH2`) and `interceptors.decompress()` both
129
- // emit an ExperimentalWarning on first use, so build the whole dispatcher
130
- // inside the suppression block.
131
- return suppressExperimentalWarningsSync(() => {
132
- // `allowH2: true` opts into HTTP/2 via ALPN; undici falls back to h1.1
133
- // when the server doesn't negotiate h2. Without this flag undici
134
- // defaults to h1.1 even when the server supports h2.
135
- const agent = new EnvHttpProxyAgent({ allowH2: true });
136
- // Some runtimes report `process.versions.node` (so `isNodeEnvironment()`
137
- // passes) but ship only a partial undici: `interceptors.decompress` is
138
- // absent and dispatchers have no `.compose`. Bun is the common case.
139
- // There the proxy agent alone is enough — Bun's `fetch` decompresses
140
- // gzip/deflate/br/zstd natively — so skip the interceptor instead of
141
- // crashing on the missing API. Optional chaining also guards a runtime
142
- // that omits the `interceptors` export entirely.
143
- if (typeof interceptors?.decompress !== 'function') {
144
- // Bun: pair the agent with the global `fetch` (undefined), which
145
- // decompresses natively.
146
- return { dispatcher: agent, fetch: undefined };
147
- }
148
- // `interceptors.decompress()` decodes gzip/deflate/br/zstd bodies. On
149
- // Node 24+, attaching any custom dispatcher to global `fetch` strips
150
- // `content-encoding` without actually decompressing the body.
151
- // See https://github.com/Doist/todoist-cli/issues/318.
152
- const decompress = interceptors.decompress();
153
- // Pair undici's own `fetch` with this dispatcher so the request client
154
- // and the dispatcher stay on one undici version (see `getDefaultFetch`).
155
- // The global `fetch` is backed by a different, Node-bundled undici;
156
- // mixing the two makes the decompress interceptor terminate gzip
157
- // responses on some Node versions.
158
- return { dispatcher: agent.compose(decompress), fetch: undiciFetch };
159
- });
160
- }
161
- // `suppressExperimentalWarningsSync` is exported for direct unit testing —
162
- // the integration path through `getDefaultDispatcher()` can't reliably
163
- // exercise it because both the dispatcher singleton and undici's internal
164
- // `warningEmitted` flag are once-per-process.
165
- //
166
- // `fn` must be synchronous so the override covers a single critical section
167
- // (microseconds) — no unrelated `ExperimentalWarning` from elsewhere can
168
- // interleave and be lost. We suppress every `ExperimentalWarning` rather than
169
- // pattern-matching the message text: the message wording is an undici
170
- // implementation detail (not a stable API), and the suppression window is
171
- // narrow enough that a coarse type filter is safe.
172
- function suppressExperimentalWarningsSync(fn) {
173
- const originalEmit = process.emitWarning;
174
- process.emitWarning = ((warning, typeOrOptions, ...rest) => {
175
- const type = typeof typeOrOptions === 'string'
176
- ? typeOrOptions
177
- : typeof typeOrOptions === 'object' && typeOrOptions !== null
178
- ? typeOrOptions.type
179
- : undefined;
180
- if (type === 'ExperimentalWarning')
181
- return;
182
- originalEmit.call(process, warning, typeOrOptions, ...rest);
183
- });
184
- try {
185
- return fn();
186
- }
187
- finally {
188
- process.emitWarning = originalEmit;
189
- }
190
- }
@@ -1,8 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_API_VERSION = exports.API_VERSIONS = void 0;
4
- /**
5
- * Supported Comms API versions
6
- */
7
- exports.API_VERSIONS = ['v1'];
8
- exports.DEFAULT_API_VERSION = 'v1';