@doist/comms-sdk 0.1.0-alpha.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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +143 -0
  3. package/dist/cjs/authentication.js +211 -0
  4. package/dist/cjs/clients/add-comment-helper.js +53 -0
  5. package/dist/cjs/clients/base-client.js +27 -0
  6. package/dist/cjs/clients/channels-client.js +83 -0
  7. package/dist/cjs/clients/comments-client.js +93 -0
  8. package/dist/cjs/clients/conversation-messages-client.js +87 -0
  9. package/dist/cjs/clients/conversations-client.js +103 -0
  10. package/dist/cjs/clients/groups-client.js +71 -0
  11. package/dist/cjs/clients/inbox-client.js +98 -0
  12. package/dist/cjs/clients/reactions-client.js +59 -0
  13. package/dist/cjs/clients/search-client.js +88 -0
  14. package/dist/cjs/clients/threads-client.js +135 -0
  15. package/dist/cjs/clients/users-client.js +199 -0
  16. package/dist/cjs/clients/workspace-users-client.js +140 -0
  17. package/dist/cjs/clients/workspaces-client.js +93 -0
  18. package/dist/cjs/comms-api.js +65 -0
  19. package/dist/cjs/consts/endpoints.js +27 -0
  20. package/dist/cjs/index.js +48 -0
  21. package/dist/cjs/package.json +1 -0
  22. package/dist/cjs/testUtils/msw-handlers.js +51 -0
  23. package/dist/cjs/testUtils/msw-setup.js +21 -0
  24. package/dist/cjs/testUtils/obsidian-fetch-adapter.js +53 -0
  25. package/dist/cjs/testUtils/test-defaults.js +102 -0
  26. package/dist/cjs/transport/fetch-with-retry.js +136 -0
  27. package/dist/cjs/transport/http-client.js +56 -0
  28. package/dist/cjs/transport/http-dispatcher.js +143 -0
  29. package/dist/cjs/types/entities.js +411 -0
  30. package/dist/cjs/types/enums.js +37 -0
  31. package/dist/cjs/types/errors.js +12 -0
  32. package/dist/cjs/types/http.js +4 -0
  33. package/dist/cjs/types/index.js +21 -0
  34. package/dist/cjs/types/requests.js +117 -0
  35. package/dist/cjs/utils/case-conversion.js +54 -0
  36. package/dist/cjs/utils/index.js +19 -0
  37. package/dist/cjs/utils/timestamp-conversion.js +49 -0
  38. package/dist/cjs/utils/url-helpers.js +131 -0
  39. package/dist/cjs/utils/uuidv7.js +174 -0
  40. package/dist/esm/authentication.js +203 -0
  41. package/dist/esm/clients/add-comment-helper.js +50 -0
  42. package/dist/esm/clients/base-client.js +23 -0
  43. package/dist/esm/clients/channels-client.js +79 -0
  44. package/dist/esm/clients/comments-client.js +89 -0
  45. package/dist/esm/clients/conversation-messages-client.js +83 -0
  46. package/dist/esm/clients/conversations-client.js +99 -0
  47. package/dist/esm/clients/groups-client.js +67 -0
  48. package/dist/esm/clients/inbox-client.js +94 -0
  49. package/dist/esm/clients/reactions-client.js +55 -0
  50. package/dist/esm/clients/search-client.js +84 -0
  51. package/dist/esm/clients/threads-client.js +131 -0
  52. package/dist/esm/clients/users-client.js +195 -0
  53. package/dist/esm/clients/workspace-users-client.js +136 -0
  54. package/dist/esm/clients/workspaces-client.js +89 -0
  55. package/dist/esm/comms-api.js +61 -0
  56. package/dist/esm/consts/endpoints.js +23 -0
  57. package/dist/esm/index.js +17 -0
  58. package/dist/esm/testUtils/msw-handlers.js +45 -0
  59. package/dist/esm/testUtils/msw-setup.js +18 -0
  60. package/dist/esm/testUtils/obsidian-fetch-adapter.js +50 -0
  61. package/dist/esm/testUtils/test-defaults.js +99 -0
  62. package/dist/esm/transport/fetch-with-retry.js +133 -0
  63. package/dist/esm/transport/http-client.js +51 -0
  64. package/dist/esm/transport/http-dispatcher.js +104 -0
  65. package/dist/esm/types/entities.js +408 -0
  66. package/dist/esm/types/enums.js +34 -0
  67. package/dist/esm/types/errors.js +8 -0
  68. package/dist/esm/types/http.js +1 -0
  69. package/dist/esm/types/index.js +5 -0
  70. package/dist/esm/types/requests.js +114 -0
  71. package/dist/esm/utils/case-conversion.js +47 -0
  72. package/dist/esm/utils/index.js +3 -0
  73. package/dist/esm/utils/timestamp-conversion.js +45 -0
  74. package/dist/esm/utils/url-helpers.js +112 -0
  75. package/dist/esm/utils/uuidv7.js +163 -0
  76. package/dist/types/authentication.d.ts +160 -0
  77. package/dist/types/clients/add-comment-helper.d.ts +12 -0
  78. package/dist/types/clients/base-client.d.ts +24 -0
  79. package/dist/types/clients/channels-client.d.ts +91 -0
  80. package/dist/types/clients/comments-client.d.ts +157 -0
  81. package/dist/types/clients/conversation-messages-client.d.ts +127 -0
  82. package/dist/types/clients/conversations-client.d.ts +206 -0
  83. package/dist/types/clients/groups-client.d.ts +55 -0
  84. package/dist/types/clients/inbox-client.d.ts +20 -0
  85. package/dist/types/clients/reactions-client.d.ts +19 -0
  86. package/dist/types/clients/search-client.d.ts +20 -0
  87. package/dist/types/clients/threads-client.d.ts +344 -0
  88. package/dist/types/clients/users-client.d.ts +123 -0
  89. package/dist/types/clients/workspace-users-client.d.ts +47 -0
  90. package/dist/types/clients/workspaces-client.d.ts +79 -0
  91. package/dist/types/comms-api.d.ts +59 -0
  92. package/dist/types/consts/endpoints.d.ts +19 -0
  93. package/dist/types/index.d.ts +18 -0
  94. package/dist/types/testUtils/msw-handlers.d.ts +28 -0
  95. package/dist/types/testUtils/msw-setup.d.ts +1 -0
  96. package/dist/types/testUtils/obsidian-fetch-adapter.d.ts +29 -0
  97. package/dist/types/testUtils/test-defaults.d.ts +16 -0
  98. package/dist/types/transport/fetch-with-retry.d.ts +4 -0
  99. package/dist/types/transport/http-client.d.ts +13 -0
  100. package/dist/types/transport/http-dispatcher.d.ts +10 -0
  101. package/dist/types/types/entities.d.ts +1288 -0
  102. package/dist/types/types/enums.d.ts +55 -0
  103. package/dist/types/types/errors.d.ts +6 -0
  104. package/dist/types/types/http.d.ts +54 -0
  105. package/dist/types/types/index.d.ts +5 -0
  106. package/dist/types/types/requests.d.ts +385 -0
  107. package/dist/types/utils/case-conversion.d.ts +8 -0
  108. package/dist/types/utils/index.d.ts +3 -0
  109. package/dist/types/utils/timestamp-conversion.d.ts +13 -0
  110. package/dist/types/utils/url-helpers.d.ts +88 -0
  111. package/dist/types/utils/uuidv7.d.ts +40 -0
  112. package/package.json +93 -0
@@ -0,0 +1,53 @@
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
+ }
@@ -0,0 +1,102 @@
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_TOKEN = void 0;
4
+ exports.TEST_API_TOKEN = 'test-api-token';
5
+ // Canonical test IDs — shaped so they pass SDK-side validation.
6
+ exports.TEST_CHANNEL_ID = '7YpL3oZ4kZ9vP7Q1tR2sX3y';
7
+ exports.TEST_THREAD_ID = '7YpL3oZ4kZ9vP7Q1tR2sX3z';
8
+ exports.TEST_COMMENT_ID = '7YpL3oZ4kZ9vP7Q1tR2sX41';
9
+ exports.TEST_CONVERSATION_ID = '7YpL3oZ4kZ9vP7Q1tR2sX42';
10
+ exports.TEST_MESSAGE_ID = '7YpL3oZ4kZ9vP7Q1tR2sX43';
11
+ exports.TEST_GROUP_ID = '7YpL3oZ4kZ9vP7Q1tR2sX44';
12
+ exports.mockUser = {
13
+ id: 1,
14
+ email: 'test@example.com',
15
+ fullName: 'Test User',
16
+ shortName: 'TU',
17
+ timezone: 'America/New_York',
18
+ removed: false,
19
+ lang: 'en',
20
+ };
21
+ exports.mockWorkspace = {
22
+ id: 1,
23
+ name: 'Test Workspace',
24
+ creator: 1,
25
+ created: new Date('2021-01-01T00:00:00Z'),
26
+ };
27
+ exports.mockChannel = {
28
+ id: exports.TEST_CHANNEL_ID,
29
+ name: 'general',
30
+ creator: 1,
31
+ public: true,
32
+ workspaceId: 1,
33
+ archived: false,
34
+ created: new Date('2021-01-01T00:00:00Z'),
35
+ version: 0,
36
+ url: `https://comms.todoist.com/a/1/ch/${exports.TEST_CHANNEL_ID}/`,
37
+ };
38
+ exports.mockThread = {
39
+ id: exports.TEST_THREAD_ID,
40
+ title: 'Test Thread',
41
+ content: 'This is a test thread',
42
+ creator: 1,
43
+ channelId: exports.TEST_CHANNEL_ID,
44
+ workspaceId: 1,
45
+ commentCount: 0,
46
+ lastUpdated: new Date('2021-01-01T00:00:00Z'),
47
+ pinned: false,
48
+ posted: new Date('2021-01-01T00:00:00Z'),
49
+ snippet: 'This is a test thread',
50
+ snippetCreator: 1,
51
+ isArchived: false,
52
+ url: `https://comms.todoist.com/a/1/ch/${exports.TEST_CHANNEL_ID}/t/${exports.TEST_THREAD_ID}/`,
53
+ };
54
+ exports.mockGroup = {
55
+ id: exports.TEST_GROUP_ID,
56
+ name: 'Test Group',
57
+ workspaceId: 1,
58
+ userIds: [1, 2, 3],
59
+ version: 0,
60
+ };
61
+ exports.mockConversation = {
62
+ id: exports.TEST_CONVERSATION_ID,
63
+ workspaceId: 1,
64
+ userIds: [1, 2],
65
+ messageCount: 1,
66
+ lastObjIndex: 0,
67
+ snippet: 'Hello there',
68
+ snippetCreators: [1],
69
+ lastActive: new Date('2021-01-01T00:00:00Z'),
70
+ archived: false,
71
+ created: new Date('2021-01-01T00:00:00Z'),
72
+ creator: 1,
73
+ url: `https://comms.todoist.com/a/1/msg/${exports.TEST_CONVERSATION_ID}/`,
74
+ };
75
+ exports.mockComment = {
76
+ id: exports.TEST_COMMENT_ID,
77
+ content: 'This is a comment',
78
+ creator: 1,
79
+ threadId: exports.TEST_THREAD_ID,
80
+ workspaceId: 1,
81
+ channelId: exports.TEST_CHANNEL_ID,
82
+ posted: new Date('2021-01-01T00:00:00Z'),
83
+ url: `https://comms.todoist.com/a/1/ch/${exports.TEST_CHANNEL_ID}/t/${exports.TEST_THREAD_ID}/c/${exports.TEST_COMMENT_ID}`,
84
+ };
85
+ exports.mockWorkspaceUser = {
86
+ id: 1,
87
+ fullName: 'Test User',
88
+ email: 'test@example.com',
89
+ userType: 'USER',
90
+ shortName: 'TU',
91
+ firstName: 'Test',
92
+ imageId: null,
93
+ avatarUrls: null,
94
+ dateFormat: null,
95
+ removed: false,
96
+ restricted: null,
97
+ setupPending: null,
98
+ theme: null,
99
+ timeFormat: null,
100
+ timezone: 'America/New_York',
101
+ version: 1,
102
+ };
@@ -0,0 +1,136 @@
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
+ const dispatcher = await (0, http_dispatcher_1.getDefaultDispatcher)();
66
+ const response = dispatcher
67
+ ? await fetch(url, {
68
+ ...options,
69
+ signal,
70
+ // @ts-expect-error - dispatcher is valid for Node.js fetch but not in TS types
71
+ dispatcher,
72
+ })
73
+ : await fetch(url, {
74
+ ...options,
75
+ signal,
76
+ });
77
+ return convertResponseToCustomFetch(response);
78
+ }
79
+ async function sleep(ms) {
80
+ return new Promise((resolve) => setTimeout(resolve, ms));
81
+ }
82
+ const timeoutErrorName = 'TimeoutError';
83
+ function createTimeoutError(timeoutMs) {
84
+ const error = new Error(`Request timeout after ${timeoutMs}ms`);
85
+ error.name = timeoutErrorName;
86
+ return error;
87
+ }
88
+ function isNetworkError(error) {
89
+ return (error.name === 'TypeError' ||
90
+ error.name === timeoutErrorName ||
91
+ error.message.toLowerCase().includes('network'));
92
+ }
93
+ function getRetryDelay(retryCount) {
94
+ return retryCount === 1 ? 0 : 500;
95
+ }
96
+ function createTimeoutSignal(timeoutMs, existingSignal) {
97
+ const controller = new AbortController();
98
+ const timeoutId = setTimeout(() => {
99
+ controller.abort(createTimeoutError(timeoutMs));
100
+ }, timeoutMs);
101
+ let abortHandler;
102
+ function clear() {
103
+ clearTimeout(timeoutId);
104
+ if (existingSignal && abortHandler) {
105
+ existingSignal.removeEventListener('abort', abortHandler);
106
+ }
107
+ }
108
+ if (existingSignal) {
109
+ if (existingSignal.aborted) {
110
+ clearTimeout(timeoutId);
111
+ controller.abort(existingSignal.reason);
112
+ }
113
+ else {
114
+ abortHandler = () => {
115
+ clearTimeout(timeoutId);
116
+ controller.abort(existingSignal.reason);
117
+ };
118
+ existingSignal.addEventListener('abort', abortHandler, { once: true });
119
+ }
120
+ }
121
+ return { signal: controller.signal, clear };
122
+ }
123
+ function convertResponseToCustomFetch(response) {
124
+ const headers = {};
125
+ response.headers.forEach((value, key) => {
126
+ headers[key] = value;
127
+ });
128
+ return {
129
+ ok: response.ok,
130
+ status: response.status,
131
+ statusText: response.statusText,
132
+ headers,
133
+ text: () => response.clone().text(),
134
+ json: () => response.json(),
135
+ };
136
+ }
@@ -0,0 +1,56 @@
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
+ }
@@ -0,0 +1,143 @@
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.getDefaultDispatcher = getDefaultDispatcher;
37
+ exports.closeDefaultDispatcher = closeDefaultDispatcher;
38
+ exports.resetDefaultDispatcherForTests = resetDefaultDispatcherForTests;
39
+ exports.suppressExperimentalWarningsSync = suppressExperimentalWarningsSync;
40
+ let defaultDispatcher;
41
+ let defaultDispatcherPromise;
42
+ async function getDefaultDispatcher() {
43
+ if (defaultDispatcher) {
44
+ return defaultDispatcher;
45
+ }
46
+ if (!defaultDispatcherPromise) {
47
+ defaultDispatcherPromise = createDefaultDispatcher()
48
+ .then((dispatcher) => {
49
+ defaultDispatcher = dispatcher;
50
+ return dispatcher;
51
+ })
52
+ .catch((error) => {
53
+ defaultDispatcher = undefined;
54
+ defaultDispatcherPromise = undefined;
55
+ throw error;
56
+ });
57
+ }
58
+ return defaultDispatcherPromise;
59
+ }
60
+ /**
61
+ * Drains the default dispatcher's connection pool. CLIs and scripts should
62
+ * `await` this before exit so Node's event loop empties immediately instead
63
+ * of waiting ~4s on keep-alive. No-op in the browser branch.
64
+ */
65
+ async function closeDefaultDispatcher() {
66
+ // Clear the singleton *before* awaiting init, so any concurrent
67
+ // `getDefaultDispatcher()` after this point creates a fresh dispatcher
68
+ // instead of receiving a reference to the one we're about to close.
69
+ const initPromise = defaultDispatcherPromise;
70
+ defaultDispatcher = undefined;
71
+ defaultDispatcherPromise = undefined;
72
+ if (!initPromise)
73
+ return;
74
+ try {
75
+ const dispatcher = await initPromise;
76
+ if (dispatcher) {
77
+ await dispatcher.close();
78
+ }
79
+ }
80
+ catch {
81
+ // init failed; nothing to close
82
+ }
83
+ }
84
+ function resetDefaultDispatcherForTests() {
85
+ defaultDispatcher = undefined;
86
+ defaultDispatcherPromise = undefined;
87
+ }
88
+ function isNodeEnvironment() {
89
+ return typeof process !== 'undefined' && typeof process.versions?.node === 'string';
90
+ }
91
+ async function createDefaultDispatcher() {
92
+ if (!isNodeEnvironment()) {
93
+ return undefined;
94
+ }
95
+ // Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
96
+ // undici into their bundle. `isNodeEnvironment()` above already gated this
97
+ // branch, so undici is safe to load when we get here.
98
+ const { EnvHttpProxyAgent, interceptors } = await Promise.resolve().then(() => __importStar(require('undici')));
99
+ // `allowH2: true` opts into HTTP/2 via ALPN; undici falls back to h1.1
100
+ // when the server doesn't negotiate h2. Without this flag undici
101
+ // defaults to h1.1 even when the server supports h2.
102
+ //
103
+ // `interceptors.decompress()` decodes gzip/deflate/br/zstd bodies. On
104
+ // Node 24+, attaching any custom dispatcher to global `fetch` strips
105
+ // `content-encoding` without actually decompressing the body.
106
+ // See https://github.com/Doist/todoist-cli/issues/318.
107
+ //
108
+ // Both emit ExperimentalWarning on first use; suppressed during init.
109
+ return suppressExperimentalWarningsSync(() => {
110
+ const decompress = interceptors.decompress();
111
+ return new EnvHttpProxyAgent({ allowH2: true }).compose(decompress);
112
+ });
113
+ }
114
+ // `suppressExperimentalWarningsSync` is exported for direct unit testing —
115
+ // the integration path through `getDefaultDispatcher()` can't reliably
116
+ // exercise it because both the dispatcher singleton and undici's internal
117
+ // `warningEmitted` flag are once-per-process.
118
+ //
119
+ // `fn` must be synchronous so the override covers a single critical section
120
+ // (microseconds) — no unrelated `ExperimentalWarning` from elsewhere can
121
+ // interleave and be lost. We suppress every `ExperimentalWarning` rather than
122
+ // pattern-matching the message text: the message wording is an undici
123
+ // implementation detail (not a stable API), and the suppression window is
124
+ // narrow enough that a coarse type filter is safe.
125
+ function suppressExperimentalWarningsSync(fn) {
126
+ const originalEmit = process.emitWarning;
127
+ process.emitWarning = ((warning, typeOrOptions, ...rest) => {
128
+ const type = typeof typeOrOptions === 'string'
129
+ ? typeOrOptions
130
+ : typeof typeOrOptions === 'object' && typeOrOptions !== null
131
+ ? typeOrOptions.type
132
+ : undefined;
133
+ if (type === 'ExperimentalWarning')
134
+ return;
135
+ originalEmit.call(process, warning, typeOrOptions, ...rest);
136
+ });
137
+ try {
138
+ return fn();
139
+ }
140
+ finally {
141
+ process.emitWarning = originalEmit;
142
+ }
143
+ }