@doist/comms-sdk 0.7.0 → 0.8.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.
- package/dist/cjs/clients/add-comment-helper.js +2 -32
- package/dist/cjs/clients/notify-audience.js +45 -0
- package/dist/cjs/clients/threads-client.js +9 -1
- package/dist/cjs/testUtils/msw-setup.js +14 -0
- package/dist/cjs/transport/fetch-with-retry.js +15 -5
- package/dist/cjs/transport/http-dispatcher.js +62 -26
- package/dist/cjs/types/requests.js +1 -0
- package/dist/esm/clients/add-comment-helper.js +1 -31
- package/dist/esm/clients/notify-audience.js +42 -0
- package/dist/esm/clients/threads-client.js +9 -1
- package/dist/esm/testUtils/msw-setup.js +15 -1
- package/dist/esm/transport/fetch-with-retry.js +16 -6
- package/dist/esm/transport/http-dispatcher.js +60 -26
- package/dist/esm/types/requests.js +1 -0
- package/dist/types/clients/notify-audience.d.ts +22 -0
- package/dist/types/clients/threads-client.d.ts +6 -0
- package/dist/types/transport/http-dispatcher.d.ts +36 -0
- package/dist/types/types/requests.d.ts +4 -0
- package/package.json +1 -1
|
@@ -3,38 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.addCommentRequest = addCommentRequest;
|
|
4
4
|
const endpoints_1 = require("../consts/endpoints");
|
|
5
5
|
const http_client_1 = require("../transport/http-client");
|
|
6
|
-
const enums_1 = require("../types/enums");
|
|
7
6
|
const uuidv7_1 = require("../utils/uuidv7");
|
|
8
|
-
const
|
|
9
|
-
function isNotifyAudience(value) {
|
|
10
|
-
return typeof value === 'string' && enums_1.NOTIFY_AUDIENCES.includes(value);
|
|
11
|
-
}
|
|
12
|
-
function collectMarkerOffenses(field, values) {
|
|
13
|
-
if (!values)
|
|
14
|
-
return null;
|
|
15
|
-
const offending = values.filter((id) => SENTINEL_GROUP_IDS.has(id));
|
|
16
|
-
return offending.length > 0 ? { field, offending } : null;
|
|
17
|
-
}
|
|
18
|
-
function applyNotifyAudience(params) {
|
|
19
|
-
const offenses = [
|
|
20
|
-
collectMarkerOffenses('groups', params.groups ?? undefined),
|
|
21
|
-
collectMarkerOffenses('directGroupMentions', params.directGroupMentions ?? undefined),
|
|
22
|
-
].filter((o) => o !== null);
|
|
23
|
-
if (offenses.length > 0) {
|
|
24
|
-
const details = offenses
|
|
25
|
-
.map(({ field, offending }) => `\`${field}\` contains ${offending.join(', ')}`)
|
|
26
|
-
.join('; ');
|
|
27
|
-
throw new Error(`Reserved broadcast marker IDs found: ${details}. Pass these via \`notifyAudience\` on createComment / closeThread / reopenThread (e.g. \`notifyAudience: 'channel'\` for EVERYONE) instead of populating \`groups\` / \`directGroupMentions\` directly.`);
|
|
28
|
-
}
|
|
29
|
-
if (params.notifyAudience == null)
|
|
30
|
-
return params;
|
|
31
|
-
if (!isNotifyAudience(params.notifyAudience)) {
|
|
32
|
-
throw new Error(`Invalid \`notifyAudience\` value "${String(params.notifyAudience)}". Expected one of: ${enums_1.NOTIFY_AUDIENCES.join(', ')}.`);
|
|
33
|
-
}
|
|
34
|
-
const sentinel = enums_1.NOTIFY_AUDIENCE_GROUP_IDS[params.notifyAudience];
|
|
35
|
-
const { notifyAudience: _stripped, groups, ...rest } = params;
|
|
36
|
-
return { ...rest, groups: [...(groups ?? []), sentinel] };
|
|
37
|
-
}
|
|
7
|
+
const notify_audience_1 = require("./notify-audience");
|
|
38
8
|
/**
|
|
39
9
|
* Internal helper that powers `comments.createComment`,
|
|
40
10
|
* `threads.closeThread`, and `threads.reopenThread`.
|
|
@@ -53,7 +23,7 @@ function applyNotifyAudience(params) {
|
|
|
53
23
|
* @returns The parsed {@link Comment} returned by the API.
|
|
54
24
|
*/
|
|
55
25
|
function addCommentRequest(context, params, options) {
|
|
56
|
-
const normalized = applyNotifyAudience(params);
|
|
26
|
+
const normalized = (0, notify_audience_1.applyNotifyAudience)(params);
|
|
57
27
|
const withId = { ...normalized, id: (0, uuidv7_1.resolveCreateId)(normalized.id) };
|
|
58
28
|
const payload = options?.threadAction
|
|
59
29
|
? { ...withId, threadAction: options.threadAction }
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyNotifyAudience = applyNotifyAudience;
|
|
4
|
+
const enums_1 = require("../types/enums");
|
|
5
|
+
const SENTINEL_GROUP_IDS = new Set(Object.values(enums_1.NOTIFY_AUDIENCE_GROUP_IDS));
|
|
6
|
+
function isNotifyAudience(value) {
|
|
7
|
+
return typeof value === 'string' && enums_1.NOTIFY_AUDIENCES.includes(value);
|
|
8
|
+
}
|
|
9
|
+
function collectMarkerOffenses(field, values) {
|
|
10
|
+
if (!values)
|
|
11
|
+
return null;
|
|
12
|
+
const offending = values.filter((id) => SENTINEL_GROUP_IDS.has(id));
|
|
13
|
+
return offending.length > 0 ? { field, offending } : null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Normalizes a `notifyAudience` flag into a sentinel `groups` entry
|
|
17
|
+
* (`EVERYONE` / `EVERYONE_IN_THREAD`) and rejects callers who put those
|
|
18
|
+
* reserved markers directly into `groups` / `directGroupMentions`.
|
|
19
|
+
*
|
|
20
|
+
* When `notifyAudience` is set it is stripped and its sentinel is appended to
|
|
21
|
+
* `groups`; the backend only understands the marker in `groups`. When it is
|
|
22
|
+
* unset the params are returned unchanged, preserving any explicit
|
|
23
|
+
* `groups` / `directGroupMentions` (including a `null`) on the wire.
|
|
24
|
+
*/
|
|
25
|
+
function applyNotifyAudience(params) {
|
|
26
|
+
const offenses = [
|
|
27
|
+
collectMarkerOffenses('groups', params.groups ?? undefined),
|
|
28
|
+
collectMarkerOffenses('directGroupMentions', params.directGroupMentions ?? undefined),
|
|
29
|
+
].filter((o) => o !== null);
|
|
30
|
+
if (offenses.length > 0) {
|
|
31
|
+
const details = offenses
|
|
32
|
+
.map(({ field, offending }) => `\`${field}\` contains ${offending.join(', ')}`)
|
|
33
|
+
.join('; ');
|
|
34
|
+
throw new Error(`Reserved broadcast marker IDs found: ${details}. Pass these via \`notifyAudience\` (e.g. \`notifyAudience: 'channel'\` for EVERYONE) instead of populating \`groups\` / \`directGroupMentions\` directly.`);
|
|
35
|
+
}
|
|
36
|
+
if (params.notifyAudience == null) {
|
|
37
|
+
return params;
|
|
38
|
+
}
|
|
39
|
+
if (!isNotifyAudience(params.notifyAudience)) {
|
|
40
|
+
throw new Error(`Invalid \`notifyAudience\` value "${String(params.notifyAudience)}". Expected one of: ${enums_1.NOTIFY_AUDIENCES.join(', ')}.`);
|
|
41
|
+
}
|
|
42
|
+
const sentinel = enums_1.NOTIFY_AUDIENCE_GROUP_IDS[params.notifyAudience];
|
|
43
|
+
const { notifyAudience: _stripped, groups, ...rest } = params;
|
|
44
|
+
return { ...rest, groups: [...(groups ?? []), sentinel] };
|
|
45
|
+
}
|
|
@@ -8,6 +8,7 @@ const entities_1 = require("../types/entities");
|
|
|
8
8
|
const uuidv7_1 = require("../utils/uuidv7");
|
|
9
9
|
const add_comment_helper_1 = require("./add-comment-helper");
|
|
10
10
|
const base_client_1 = require("./base-client");
|
|
11
|
+
const notify_audience_1 = require("./notify-audience");
|
|
11
12
|
exports.ThreadListSchema = zod_1.z.array(entities_1.ThreadSchema);
|
|
12
13
|
const GetUnreadResponseSchema = zod_1.z.object({
|
|
13
14
|
data: zod_1.z.array(entities_1.UnreadThreadSchema),
|
|
@@ -95,6 +96,11 @@ class ThreadsClient extends base_client_1.BaseClient {
|
|
|
95
96
|
* @param args.content - The thread content.
|
|
96
97
|
* @param args.recipients - Optional array of user IDs to notify.
|
|
97
98
|
* @param args.groups - Optional array of custom group IDs to notify.
|
|
99
|
+
* @param args.notifyAudience - Optional broader audience to notify in addition to
|
|
100
|
+
* `recipients` and `groups`. `'channel'` notifies everyone in the channel;
|
|
101
|
+
* `'thread'` notifies everyone who has interacted with the thread — but a
|
|
102
|
+
* brand-new thread has no interactions yet, so the backend discards
|
|
103
|
+
* `'thread'` at create time. It is only meaningful on replies (`createComment`).
|
|
98
104
|
* @param args.attachments - Optional array of {@link Attachment}s (from `attachments.upload`).
|
|
99
105
|
* @returns The created thread object.
|
|
100
106
|
*
|
|
@@ -104,11 +110,13 @@ class ThreadsClient extends base_client_1.BaseClient {
|
|
|
104
110
|
* channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44',
|
|
105
111
|
* title: 'New Feature Discussion',
|
|
106
112
|
* content: 'Let\'s discuss the new feature...',
|
|
113
|
+
* notifyAudience: 'channel', // tag "Everyone in channel"
|
|
107
114
|
* })
|
|
108
115
|
* ```
|
|
109
116
|
*/
|
|
110
117
|
createThread(args) {
|
|
111
|
-
|
|
118
|
+
const normalized = (0, notify_audience_1.applyNotifyAudience)(args);
|
|
119
|
+
return this.simple('POST', 'add', { ...normalized, id: (0, uuidv7_1.resolveCreateId)(args.id) }, this.threadSchema);
|
|
112
120
|
}
|
|
113
121
|
/**
|
|
114
122
|
* Partial update of an existing thread.
|
|
@@ -3,6 +3,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.server = void 0;
|
|
4
4
|
const node_1 = require("msw/node");
|
|
5
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
|
+
});
|
|
6
20
|
// Create MSW server instance
|
|
7
21
|
exports.server = (0, node_1.setupServer)();
|
|
8
22
|
// Start server before all tests
|
|
@@ -62,15 +62,25 @@ async function fetchWithRetry(url, options, maxRetries = 3, customFetch) {
|
|
|
62
62
|
throw new errors_1.CommsRequestError(lastError?.message ?? 'Request failed');
|
|
63
63
|
}
|
|
64
64
|
async function fetchWithDefaultTransport(url, options, signal) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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, {
|
|
68
78
|
...options,
|
|
69
79
|
signal,
|
|
70
80
|
// @ts-expect-error - dispatcher is valid for Node.js fetch but not in TS types
|
|
71
|
-
dispatcher,
|
|
81
|
+
dispatcher: transport.dispatcher,
|
|
72
82
|
})
|
|
73
|
-
: await
|
|
83
|
+
: await fetchImpl(url, {
|
|
74
84
|
...options,
|
|
75
85
|
signal,
|
|
76
86
|
});
|
|
@@ -33,29 +33,39 @@ 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;
|
|
37
38
|
exports.closeDefaultDispatcher = closeDefaultDispatcher;
|
|
38
39
|
exports.resetDefaultDispatcherForTests = resetDefaultDispatcherForTests;
|
|
40
|
+
exports.getDefaultFetch = getDefaultFetch;
|
|
39
41
|
exports.suppressExperimentalWarningsSync = suppressExperimentalWarningsSync;
|
|
40
|
-
let
|
|
41
|
-
let
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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;
|
|
45
52
|
}
|
|
46
|
-
if (!
|
|
47
|
-
|
|
48
|
-
.then((
|
|
49
|
-
|
|
50
|
-
return
|
|
53
|
+
if (!defaultTransportPromise) {
|
|
54
|
+
defaultTransportPromise = createDefaultTransport()
|
|
55
|
+
.then((transport) => {
|
|
56
|
+
defaultTransport = transport;
|
|
57
|
+
return transport;
|
|
51
58
|
})
|
|
52
59
|
.catch((error) => {
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
defaultTransport = undefined;
|
|
61
|
+
defaultTransportPromise = undefined;
|
|
55
62
|
throw error;
|
|
56
63
|
});
|
|
57
64
|
}
|
|
58
|
-
return
|
|
65
|
+
return defaultTransportPromise;
|
|
66
|
+
}
|
|
67
|
+
async function getDefaultDispatcher() {
|
|
68
|
+
return (await getDefaultTransport())?.dispatcher;
|
|
59
69
|
}
|
|
60
70
|
/**
|
|
61
71
|
* Drains the default dispatcher's connection pool. CLIs and scripts should
|
|
@@ -64,17 +74,17 @@ async function getDefaultDispatcher() {
|
|
|
64
74
|
*/
|
|
65
75
|
async function closeDefaultDispatcher() {
|
|
66
76
|
// Clear the singleton *before* awaiting init, so any concurrent
|
|
67
|
-
// `
|
|
77
|
+
// `getDefaultTransport()` after this point creates a fresh dispatcher
|
|
68
78
|
// instead of receiving a reference to the one we're about to close.
|
|
69
|
-
const initPromise =
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
const initPromise = defaultTransportPromise;
|
|
80
|
+
defaultTransport = undefined;
|
|
81
|
+
defaultTransportPromise = undefined;
|
|
72
82
|
if (!initPromise)
|
|
73
83
|
return;
|
|
74
84
|
try {
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
77
|
-
await dispatcher.close();
|
|
85
|
+
const transport = await initPromise;
|
|
86
|
+
if (transport) {
|
|
87
|
+
await transport.dispatcher.close();
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
catch {
|
|
@@ -82,20 +92,39 @@ async function closeDefaultDispatcher() {
|
|
|
82
92
|
}
|
|
83
93
|
}
|
|
84
94
|
function resetDefaultDispatcherForTests() {
|
|
85
|
-
|
|
86
|
-
|
|
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;
|
|
87
116
|
}
|
|
88
117
|
function isNodeEnvironment() {
|
|
89
118
|
return typeof process !== 'undefined' && typeof process.versions?.node === 'string';
|
|
90
119
|
}
|
|
91
|
-
async function
|
|
120
|
+
async function createDefaultTransport() {
|
|
92
121
|
if (!isNodeEnvironment()) {
|
|
93
122
|
return undefined;
|
|
94
123
|
}
|
|
95
124
|
// Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
|
|
96
125
|
// undici into their bundle. `isNodeEnvironment()` above already gated this
|
|
97
126
|
// branch, so undici is safe to load when we get here.
|
|
98
|
-
const { EnvHttpProxyAgent, interceptors } = await Promise.resolve().then(() => __importStar(require('undici')));
|
|
127
|
+
const { EnvHttpProxyAgent, interceptors, fetch: undiciFetch } = await Promise.resolve().then(() => __importStar(require('undici')));
|
|
99
128
|
// `EnvHttpProxyAgent` (with `allowH2`) and `interceptors.decompress()` both
|
|
100
129
|
// emit an ExperimentalWarning on first use, so build the whole dispatcher
|
|
101
130
|
// inside the suppression block.
|
|
@@ -112,14 +141,21 @@ async function createDefaultDispatcher() {
|
|
|
112
141
|
// crashing on the missing API. Optional chaining also guards a runtime
|
|
113
142
|
// that omits the `interceptors` export entirely.
|
|
114
143
|
if (typeof interceptors?.decompress !== 'function') {
|
|
115
|
-
|
|
144
|
+
// Bun: pair the agent with the global `fetch` (undefined), which
|
|
145
|
+
// decompresses natively.
|
|
146
|
+
return { dispatcher: agent, fetch: undefined };
|
|
116
147
|
}
|
|
117
148
|
// `interceptors.decompress()` decodes gzip/deflate/br/zstd bodies. On
|
|
118
149
|
// Node 24+, attaching any custom dispatcher to global `fetch` strips
|
|
119
150
|
// `content-encoding` without actually decompressing the body.
|
|
120
151
|
// See https://github.com/Doist/todoist-cli/issues/318.
|
|
121
152
|
const decompress = interceptors.decompress();
|
|
122
|
-
|
|
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 };
|
|
123
159
|
});
|
|
124
160
|
}
|
|
125
161
|
// `suppressExperimentalWarningsSync` is exported for direct unit testing —
|
|
@@ -42,6 +42,7 @@ exports.CreateThreadArgsSchema = zod_1.z.object({
|
|
|
42
42
|
id: zod_1.z.string().optional(),
|
|
43
43
|
recipients: zod_1.z.array(zod_1.z.number()).nullable().optional(),
|
|
44
44
|
groups: zod_1.z.array(zod_1.z.string()).nullable().optional(),
|
|
45
|
+
notifyAudience: zod_1.z.enum(enums_1.NOTIFY_AUDIENCES).nullable().optional(),
|
|
45
46
|
attachments: zod_1.z.array(entities_1.RequestAttachmentSchema).nullable().optional(),
|
|
46
47
|
});
|
|
47
48
|
exports.UpdateThreadArgsSchema = zod_1.z.object({
|
|
@@ -1,37 +1,7 @@
|
|
|
1
1
|
import { ENDPOINT_COMMENTS } from '../consts/endpoints.js';
|
|
2
2
|
import { request } from '../transport/http-client.js';
|
|
3
|
-
import { NOTIFY_AUDIENCE_GROUP_IDS, NOTIFY_AUDIENCES } from '../types/enums.js';
|
|
4
3
|
import { resolveCreateId } from '../utils/uuidv7.js';
|
|
5
|
-
|
|
6
|
-
function isNotifyAudience(value) {
|
|
7
|
-
return typeof value === 'string' && NOTIFY_AUDIENCES.includes(value);
|
|
8
|
-
}
|
|
9
|
-
function collectMarkerOffenses(field, values) {
|
|
10
|
-
if (!values)
|
|
11
|
-
return null;
|
|
12
|
-
const offending = values.filter((id) => SENTINEL_GROUP_IDS.has(id));
|
|
13
|
-
return offending.length > 0 ? { field, offending } : null;
|
|
14
|
-
}
|
|
15
|
-
function applyNotifyAudience(params) {
|
|
16
|
-
const offenses = [
|
|
17
|
-
collectMarkerOffenses('groups', params.groups ?? undefined),
|
|
18
|
-
collectMarkerOffenses('directGroupMentions', params.directGroupMentions ?? undefined),
|
|
19
|
-
].filter((o) => o !== null);
|
|
20
|
-
if (offenses.length > 0) {
|
|
21
|
-
const details = offenses
|
|
22
|
-
.map(({ field, offending }) => `\`${field}\` contains ${offending.join(', ')}`)
|
|
23
|
-
.join('; ');
|
|
24
|
-
throw new Error(`Reserved broadcast marker IDs found: ${details}. Pass these via \`notifyAudience\` on createComment / closeThread / reopenThread (e.g. \`notifyAudience: 'channel'\` for EVERYONE) instead of populating \`groups\` / \`directGroupMentions\` directly.`);
|
|
25
|
-
}
|
|
26
|
-
if (params.notifyAudience == null)
|
|
27
|
-
return params;
|
|
28
|
-
if (!isNotifyAudience(params.notifyAudience)) {
|
|
29
|
-
throw new Error(`Invalid \`notifyAudience\` value "${String(params.notifyAudience)}". Expected one of: ${NOTIFY_AUDIENCES.join(', ')}.`);
|
|
30
|
-
}
|
|
31
|
-
const sentinel = NOTIFY_AUDIENCE_GROUP_IDS[params.notifyAudience];
|
|
32
|
-
const { notifyAudience: _stripped, groups, ...rest } = params;
|
|
33
|
-
return { ...rest, groups: [...(groups ?? []), sentinel] };
|
|
34
|
-
}
|
|
4
|
+
import { applyNotifyAudience } from './notify-audience.js';
|
|
35
5
|
/**
|
|
36
6
|
* Internal helper that powers `comments.createComment`,
|
|
37
7
|
* `threads.closeThread`, and `threads.reopenThread`.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { NOTIFY_AUDIENCE_GROUP_IDS, NOTIFY_AUDIENCES } from '../types/enums.js';
|
|
2
|
+
const SENTINEL_GROUP_IDS = new Set(Object.values(NOTIFY_AUDIENCE_GROUP_IDS));
|
|
3
|
+
function isNotifyAudience(value) {
|
|
4
|
+
return typeof value === 'string' && NOTIFY_AUDIENCES.includes(value);
|
|
5
|
+
}
|
|
6
|
+
function collectMarkerOffenses(field, values) {
|
|
7
|
+
if (!values)
|
|
8
|
+
return null;
|
|
9
|
+
const offending = values.filter((id) => SENTINEL_GROUP_IDS.has(id));
|
|
10
|
+
return offending.length > 0 ? { field, offending } : null;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Normalizes a `notifyAudience` flag into a sentinel `groups` entry
|
|
14
|
+
* (`EVERYONE` / `EVERYONE_IN_THREAD`) and rejects callers who put those
|
|
15
|
+
* reserved markers directly into `groups` / `directGroupMentions`.
|
|
16
|
+
*
|
|
17
|
+
* When `notifyAudience` is set it is stripped and its sentinel is appended to
|
|
18
|
+
* `groups`; the backend only understands the marker in `groups`. When it is
|
|
19
|
+
* unset the params are returned unchanged, preserving any explicit
|
|
20
|
+
* `groups` / `directGroupMentions` (including a `null`) on the wire.
|
|
21
|
+
*/
|
|
22
|
+
export function applyNotifyAudience(params) {
|
|
23
|
+
const offenses = [
|
|
24
|
+
collectMarkerOffenses('groups', params.groups ?? undefined),
|
|
25
|
+
collectMarkerOffenses('directGroupMentions', params.directGroupMentions ?? undefined),
|
|
26
|
+
].filter((o) => o !== null);
|
|
27
|
+
if (offenses.length > 0) {
|
|
28
|
+
const details = offenses
|
|
29
|
+
.map(({ field, offending }) => `\`${field}\` contains ${offending.join(', ')}`)
|
|
30
|
+
.join('; ');
|
|
31
|
+
throw new Error(`Reserved broadcast marker IDs found: ${details}. Pass these via \`notifyAudience\` (e.g. \`notifyAudience: 'channel'\` for EVERYONE) instead of populating \`groups\` / \`directGroupMentions\` directly.`);
|
|
32
|
+
}
|
|
33
|
+
if (params.notifyAudience == null) {
|
|
34
|
+
return params;
|
|
35
|
+
}
|
|
36
|
+
if (!isNotifyAudience(params.notifyAudience)) {
|
|
37
|
+
throw new Error(`Invalid \`notifyAudience\` value "${String(params.notifyAudience)}". Expected one of: ${NOTIFY_AUDIENCES.join(', ')}.`);
|
|
38
|
+
}
|
|
39
|
+
const sentinel = NOTIFY_AUDIENCE_GROUP_IDS[params.notifyAudience];
|
|
40
|
+
const { notifyAudience: _stripped, groups, ...rest } = params;
|
|
41
|
+
return { ...rest, groups: [...(groups ?? []), sentinel] };
|
|
42
|
+
}
|
|
@@ -5,6 +5,7 @@ import { CommentSchema, createCommentSchema, createThreadSchema, StatusOkSchema,
|
|
|
5
5
|
import { resolveCreateId } from '../utils/uuidv7.js';
|
|
6
6
|
import { addCommentRequest } from './add-comment-helper.js';
|
|
7
7
|
import { BaseClient } from './base-client.js';
|
|
8
|
+
import { applyNotifyAudience } from './notify-audience.js';
|
|
8
9
|
export const ThreadListSchema = z.array(ThreadSchema);
|
|
9
10
|
const GetUnreadResponseSchema = z.object({
|
|
10
11
|
data: z.array(UnreadThreadSchema),
|
|
@@ -92,6 +93,11 @@ export class ThreadsClient extends BaseClient {
|
|
|
92
93
|
* @param args.content - The thread content.
|
|
93
94
|
* @param args.recipients - Optional array of user IDs to notify.
|
|
94
95
|
* @param args.groups - Optional array of custom group IDs to notify.
|
|
96
|
+
* @param args.notifyAudience - Optional broader audience to notify in addition to
|
|
97
|
+
* `recipients` and `groups`. `'channel'` notifies everyone in the channel;
|
|
98
|
+
* `'thread'` notifies everyone who has interacted with the thread — but a
|
|
99
|
+
* brand-new thread has no interactions yet, so the backend discards
|
|
100
|
+
* `'thread'` at create time. It is only meaningful on replies (`createComment`).
|
|
95
101
|
* @param args.attachments - Optional array of {@link Attachment}s (from `attachments.upload`).
|
|
96
102
|
* @returns The created thread object.
|
|
97
103
|
*
|
|
@@ -101,11 +107,13 @@ export class ThreadsClient extends BaseClient {
|
|
|
101
107
|
* channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44',
|
|
102
108
|
* title: 'New Feature Discussion',
|
|
103
109
|
* content: 'Let\'s discuss the new feature...',
|
|
110
|
+
* notifyAudience: 'channel', // tag "Everyone in channel"
|
|
104
111
|
* })
|
|
105
112
|
* ```
|
|
106
113
|
*/
|
|
107
114
|
createThread(args) {
|
|
108
|
-
|
|
115
|
+
const normalized = applyNotifyAudience(args);
|
|
116
|
+
return this.simple('POST', 'add', { ...normalized, id: resolveCreateId(args.id) }, this.threadSchema);
|
|
109
117
|
}
|
|
110
118
|
/**
|
|
111
119
|
* Partial update of an existing thread.
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { setupServer } from 'msw/node';
|
|
2
|
-
import { afterAll, afterEach, beforeAll } from 'vitest';
|
|
2
|
+
import { afterAll, afterEach, beforeAll, vi } from 'vitest';
|
|
3
|
+
// In production the transport uses undici's own `fetch` (paired with the
|
|
4
|
+
// dispatcher) so the request client and dispatcher stay on one undici version.
|
|
5
|
+
// MSW intercepts the *global* `fetch`, not that separate undici instance, so
|
|
6
|
+
// force the transport onto the global `fetch` for every test in the suite;
|
|
7
|
+
// MSW then intercepts requests as usual. The real undici transport is covered
|
|
8
|
+
// directly by `http-dispatcher.test.ts` and `fetch-with-retry.test.ts`, which
|
|
9
|
+
// opt out of this seam.
|
|
10
|
+
vi.mock('../transport/http-dispatcher', async (importOriginal) => {
|
|
11
|
+
const actual = await importOriginal();
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
getDefaultTransport: vi.fn(async () => undefined),
|
|
15
|
+
};
|
|
16
|
+
});
|
|
3
17
|
// Create MSW server instance
|
|
4
18
|
export const server = setupServer();
|
|
5
19
|
// Start server before all tests
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CommsRequestError } from '../types/errors.js';
|
|
2
2
|
import { camelCaseKeys } from '../utils/case-conversion.js';
|
|
3
3
|
import { transformTimestamps } from '../utils/timestamp-conversion.js';
|
|
4
|
-
import {
|
|
4
|
+
import { getDefaultTransport } from './http-dispatcher.js';
|
|
5
5
|
export async function fetchWithRetry(url, options, maxRetries = 3, customFetch) {
|
|
6
6
|
let lastError;
|
|
7
7
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
@@ -59,15 +59,25 @@ export async function fetchWithRetry(url, options, maxRetries = 3, customFetch)
|
|
|
59
59
|
throw new CommsRequestError(lastError?.message ?? 'Request failed');
|
|
60
60
|
}
|
|
61
61
|
async function fetchWithDefaultTransport(url, options, signal) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// Read the dispatcher and its paired `fetch` as one value so they can never
|
|
63
|
+
// be mismatched. On Node, `fetch` is undici's own — paired with the
|
|
64
|
+
// dispatcher — so the request client and dispatcher stay on one undici
|
|
65
|
+
// version; otherwise the `decompress` interceptor terminates gzip responses
|
|
66
|
+
// when the global `fetch`'s bundled undici differs. Browser/edge (and Bun)
|
|
67
|
+
// have no paired `fetch` and fall back to the global one.
|
|
68
|
+
const transport = await getDefaultTransport();
|
|
69
|
+
// undici's `fetch` and the global `fetch` are the same function at runtime
|
|
70
|
+
// but carry different (undici vs DOM) `RequestInit`/`Response` types. Call
|
|
71
|
+
// through the global signature, which matches the global-typed `options`.
|
|
72
|
+
const fetchImpl = (transport?.fetch ?? fetch);
|
|
73
|
+
const response = transport?.dispatcher
|
|
74
|
+
? await fetchImpl(url, {
|
|
65
75
|
...options,
|
|
66
76
|
signal,
|
|
67
77
|
// @ts-expect-error - dispatcher is valid for Node.js fetch but not in TS types
|
|
68
|
-
dispatcher,
|
|
78
|
+
dispatcher: transport.dispatcher,
|
|
69
79
|
})
|
|
70
|
-
: await
|
|
80
|
+
: await fetchImpl(url, {
|
|
71
81
|
...options,
|
|
72
82
|
signal,
|
|
73
83
|
});
|
|
@@ -1,22 +1,30 @@
|
|
|
1
|
-
let
|
|
2
|
-
let
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
let defaultTransport;
|
|
2
|
+
let defaultTransportPromise;
|
|
3
|
+
/**
|
|
4
|
+
* The default dispatcher and its paired `fetch`, as a single value so the two
|
|
5
|
+
* are always read consistently. Resolves to `undefined` outside Node (browser/
|
|
6
|
+
* edge), where callers use the global `fetch` with no dispatcher.
|
|
7
|
+
*/
|
|
8
|
+
export async function getDefaultTransport() {
|
|
9
|
+
if (defaultTransport) {
|
|
10
|
+
return defaultTransport;
|
|
6
11
|
}
|
|
7
|
-
if (!
|
|
8
|
-
|
|
9
|
-
.then((
|
|
10
|
-
|
|
11
|
-
return
|
|
12
|
+
if (!defaultTransportPromise) {
|
|
13
|
+
defaultTransportPromise = createDefaultTransport()
|
|
14
|
+
.then((transport) => {
|
|
15
|
+
defaultTransport = transport;
|
|
16
|
+
return transport;
|
|
12
17
|
})
|
|
13
18
|
.catch((error) => {
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
defaultTransport = undefined;
|
|
20
|
+
defaultTransportPromise = undefined;
|
|
16
21
|
throw error;
|
|
17
22
|
});
|
|
18
23
|
}
|
|
19
|
-
return
|
|
24
|
+
return defaultTransportPromise;
|
|
25
|
+
}
|
|
26
|
+
export async function getDefaultDispatcher() {
|
|
27
|
+
return (await getDefaultTransport())?.dispatcher;
|
|
20
28
|
}
|
|
21
29
|
/**
|
|
22
30
|
* Drains the default dispatcher's connection pool. CLIs and scripts should
|
|
@@ -25,17 +33,17 @@ export async function getDefaultDispatcher() {
|
|
|
25
33
|
*/
|
|
26
34
|
export async function closeDefaultDispatcher() {
|
|
27
35
|
// Clear the singleton *before* awaiting init, so any concurrent
|
|
28
|
-
// `
|
|
36
|
+
// `getDefaultTransport()` after this point creates a fresh dispatcher
|
|
29
37
|
// instead of receiving a reference to the one we're about to close.
|
|
30
|
-
const initPromise =
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
const initPromise = defaultTransportPromise;
|
|
39
|
+
defaultTransport = undefined;
|
|
40
|
+
defaultTransportPromise = undefined;
|
|
33
41
|
if (!initPromise)
|
|
34
42
|
return;
|
|
35
43
|
try {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
await dispatcher.close();
|
|
44
|
+
const transport = await initPromise;
|
|
45
|
+
if (transport) {
|
|
46
|
+
await transport.dispatcher.close();
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
catch {
|
|
@@ -43,20 +51,39 @@ export async function closeDefaultDispatcher() {
|
|
|
43
51
|
}
|
|
44
52
|
}
|
|
45
53
|
export function resetDefaultDispatcherForTests() {
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
defaultTransport = undefined;
|
|
55
|
+
defaultTransportPromise = undefined;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The `fetch` implementation that must be used with the default dispatcher.
|
|
59
|
+
* Returns undici's own `fetch` on the full-undici Node path, or `undefined`
|
|
60
|
+
* (meaning: use the global `fetch`) in the browser/edge/Bun paths.
|
|
61
|
+
*
|
|
62
|
+
* Node's global `fetch` is backed by whatever undici version ships inside that
|
|
63
|
+
* Node release (6.x on Node 22 … 8.x on Node 26). Our dispatcher — and its
|
|
64
|
+
* `decompress` interceptor — comes from the npm `undici` package, which is a
|
|
65
|
+
* different version. Handing an npm-undici dispatcher to a mismatched built-in
|
|
66
|
+
* client makes gzip responses fail mid-stream with `terminated`. Sourcing
|
|
67
|
+
* `fetch` from the same npm `undici` keeps the whole request path on one
|
|
68
|
+
* version and removes the split.
|
|
69
|
+
*
|
|
70
|
+
* Only meaningful after {@link getDefaultTransport} has resolved, which is the
|
|
71
|
+
* one place that populates it.
|
|
72
|
+
*/
|
|
73
|
+
export function getDefaultFetch() {
|
|
74
|
+
return defaultTransport?.fetch;
|
|
48
75
|
}
|
|
49
76
|
function isNodeEnvironment() {
|
|
50
77
|
return typeof process !== 'undefined' && typeof process.versions?.node === 'string';
|
|
51
78
|
}
|
|
52
|
-
async function
|
|
79
|
+
async function createDefaultTransport() {
|
|
53
80
|
if (!isNodeEnvironment()) {
|
|
54
81
|
return undefined;
|
|
55
82
|
}
|
|
56
83
|
// Dynamic import so non-Node consumers (browser, edge runtimes) don't pull
|
|
57
84
|
// undici into their bundle. `isNodeEnvironment()` above already gated this
|
|
58
85
|
// branch, so undici is safe to load when we get here.
|
|
59
|
-
const { EnvHttpProxyAgent, interceptors } = await import('undici');
|
|
86
|
+
const { EnvHttpProxyAgent, interceptors, fetch: undiciFetch } = await import('undici');
|
|
60
87
|
// `EnvHttpProxyAgent` (with `allowH2`) and `interceptors.decompress()` both
|
|
61
88
|
// emit an ExperimentalWarning on first use, so build the whole dispatcher
|
|
62
89
|
// inside the suppression block.
|
|
@@ -73,14 +100,21 @@ async function createDefaultDispatcher() {
|
|
|
73
100
|
// crashing on the missing API. Optional chaining also guards a runtime
|
|
74
101
|
// that omits the `interceptors` export entirely.
|
|
75
102
|
if (typeof interceptors?.decompress !== 'function') {
|
|
76
|
-
|
|
103
|
+
// Bun: pair the agent with the global `fetch` (undefined), which
|
|
104
|
+
// decompresses natively.
|
|
105
|
+
return { dispatcher: agent, fetch: undefined };
|
|
77
106
|
}
|
|
78
107
|
// `interceptors.decompress()` decodes gzip/deflate/br/zstd bodies. On
|
|
79
108
|
// Node 24+, attaching any custom dispatcher to global `fetch` strips
|
|
80
109
|
// `content-encoding` without actually decompressing the body.
|
|
81
110
|
// See https://github.com/Doist/todoist-cli/issues/318.
|
|
82
111
|
const decompress = interceptors.decompress();
|
|
83
|
-
|
|
112
|
+
// Pair undici's own `fetch` with this dispatcher so the request client
|
|
113
|
+
// and the dispatcher stay on one undici version (see `getDefaultFetch`).
|
|
114
|
+
// The global `fetch` is backed by a different, Node-bundled undici;
|
|
115
|
+
// mixing the two makes the decompress interceptor terminate gzip
|
|
116
|
+
// responses on some Node versions.
|
|
117
|
+
return { dispatcher: agent.compose(decompress), fetch: undiciFetch };
|
|
84
118
|
});
|
|
85
119
|
}
|
|
86
120
|
// `suppressExperimentalWarningsSync` is exported for direct unit testing —
|
|
@@ -39,6 +39,7 @@ export const CreateThreadArgsSchema = z.object({
|
|
|
39
39
|
id: z.string().optional(),
|
|
40
40
|
recipients: z.array(z.number()).nullable().optional(),
|
|
41
41
|
groups: z.array(z.string()).nullable().optional(),
|
|
42
|
+
notifyAudience: z.enum(NOTIFY_AUDIENCES).nullable().optional(),
|
|
42
43
|
attachments: z.array(RequestAttachmentSchema).nullable().optional(),
|
|
43
44
|
});
|
|
44
45
|
export const UpdateThreadArgsSchema = z.object({
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type NotifyAudience } from '../types/enums.js';
|
|
2
|
+
/**
|
|
3
|
+
* Params bearing the group-targeting fields that can carry a `notifyAudience`.
|
|
4
|
+
* Shared by the comment-creating endpoints and thread creation.
|
|
5
|
+
*/
|
|
6
|
+
type NotifyAudienceParams = {
|
|
7
|
+
groups?: string[] | null;
|
|
8
|
+
directGroupMentions?: string[] | null;
|
|
9
|
+
notifyAudience?: NotifyAudience | null;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Normalizes a `notifyAudience` flag into a sentinel `groups` entry
|
|
13
|
+
* (`EVERYONE` / `EVERYONE_IN_THREAD`) and rejects callers who put those
|
|
14
|
+
* reserved markers directly into `groups` / `directGroupMentions`.
|
|
15
|
+
*
|
|
16
|
+
* When `notifyAudience` is set it is stripped and its sentinel is appended to
|
|
17
|
+
* `groups`; the backend only understands the marker in `groups`. When it is
|
|
18
|
+
* unset the params are returned unchanged, preserving any explicit
|
|
19
|
+
* `groups` / `directGroupMentions` (including a `null`) on the wire.
|
|
20
|
+
*/
|
|
21
|
+
export declare function applyNotifyAudience<T extends NotifyAudienceParams>(params: T): Omit<T, 'notifyAudience'>;
|
|
22
|
+
export {};
|
|
@@ -336,6 +336,11 @@ export declare class ThreadsClient extends BaseClient {
|
|
|
336
336
|
* @param args.content - The thread content.
|
|
337
337
|
* @param args.recipients - Optional array of user IDs to notify.
|
|
338
338
|
* @param args.groups - Optional array of custom group IDs to notify.
|
|
339
|
+
* @param args.notifyAudience - Optional broader audience to notify in addition to
|
|
340
|
+
* `recipients` and `groups`. `'channel'` notifies everyone in the channel;
|
|
341
|
+
* `'thread'` notifies everyone who has interacted with the thread — but a
|
|
342
|
+
* brand-new thread has no interactions yet, so the backend discards
|
|
343
|
+
* `'thread'` at create time. It is only meaningful on replies (`createComment`).
|
|
339
344
|
* @param args.attachments - Optional array of {@link Attachment}s (from `attachments.upload`).
|
|
340
345
|
* @returns The created thread object.
|
|
341
346
|
*
|
|
@@ -345,6 +350,7 @@ export declare class ThreadsClient extends BaseClient {
|
|
|
345
350
|
* channelId: '7YpL3oZ4kZ9vP7Q1tR2sX44',
|
|
346
351
|
* title: 'New Feature Discussion',
|
|
347
352
|
* content: 'Let\'s discuss the new feature...',
|
|
353
|
+
* notifyAudience: 'channel', // tag "Everyone in channel"
|
|
348
354
|
* })
|
|
349
355
|
* ```
|
|
350
356
|
*/
|
|
@@ -1,4 +1,22 @@
|
|
|
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>;
|
|
3
21
|
/**
|
|
4
22
|
* Drains the default dispatcher's connection pool. CLIs and scripts should
|
|
@@ -7,4 +25,22 @@ export declare function getDefaultDispatcher(): Promise<Dispatcher | undefined>;
|
|
|
7
25
|
*/
|
|
8
26
|
export declare function closeDefaultDispatcher(): Promise<void>;
|
|
9
27
|
export declare function resetDefaultDispatcherForTests(): void;
|
|
28
|
+
/**
|
|
29
|
+
* The `fetch` implementation that must be used with the default dispatcher.
|
|
30
|
+
* Returns undici's own `fetch` on the full-undici Node path, or `undefined`
|
|
31
|
+
* (meaning: use the global `fetch`) in the browser/edge/Bun paths.
|
|
32
|
+
*
|
|
33
|
+
* Node's global `fetch` is backed by whatever undici version ships inside that
|
|
34
|
+
* Node release (6.x on Node 22 … 8.x on Node 26). Our dispatcher — and its
|
|
35
|
+
* `decompress` interceptor — comes from the npm `undici` package, which is a
|
|
36
|
+
* different version. Handing an npm-undici dispatcher to a mismatched built-in
|
|
37
|
+
* client makes gzip responses fail mid-stream with `terminated`. Sourcing
|
|
38
|
+
* `fetch` from the same npm `undici` keeps the whole request path on one
|
|
39
|
+
* version and removes the split.
|
|
40
|
+
*
|
|
41
|
+
* Only meaningful after {@link getDefaultTransport} has resolved, which is the
|
|
42
|
+
* one place that populates it.
|
|
43
|
+
*/
|
|
44
|
+
export declare function getDefaultFetch(): UndiciFetch | undefined;
|
|
10
45
|
export declare function suppressExperimentalWarningsSync<T>(fn: () => T): T;
|
|
46
|
+
export {};
|
|
@@ -40,6 +40,10 @@ export declare const CreateThreadArgsSchema: z.ZodObject<{
|
|
|
40
40
|
id: z.ZodOptional<z.ZodString>;
|
|
41
41
|
recipients: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodNumber>>>;
|
|
42
42
|
groups: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodString>>>;
|
|
43
|
+
notifyAudience: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
44
|
+
channel: "channel";
|
|
45
|
+
thread: "thread";
|
|
46
|
+
}>>>;
|
|
43
47
|
attachments: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodObject<{
|
|
44
48
|
attachmentId: z.ZodString;
|
|
45
49
|
urlType: z.ZodString;
|