@bridge_gpt/mcp-server 0.2.50 → 0.2.52
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/README.md +24 -8
- package/build/agent-capabilities/probe-context.js +15 -7
- package/build/agent-capabilities/probes.js +42 -6
- package/build/agent-launchers/claude-executor-adapter.js +98 -14
- package/build/commands.generated.js +1 -1
- package/build/conduct-epic/bridge-client.js +115 -1
- package/build/conduct-epic/cli.js +351 -33
- package/build/conduct-epic/cut-protocol.js +65 -0
- package/build/conductor/bridge-api-client.js +171 -5
- package/build/conductor/deny-enforcement-preflight.js +107 -10
- package/build/conductor/local-merge.js +170 -11
- package/build/conductor-bin.js +2 -2
- package/build/connect-bitbucket-api.js +370 -0
- package/build/connect-bitbucket.js +437 -0
- package/build/docs.generated.js +1 -1
- package/build/doctor.js +230 -1
- package/build/drive-epic.js +423 -11
- package/build/env-file-link.js +164 -0
- package/build/epic-integration-pr.js +290 -0
- package/build/executor/cli.js +41 -6
- package/build/executor/deps.js +5 -1
- package/build/executor/env-file-guard.js +113 -0
- package/build/executor/env.js +78 -1
- package/build/executor/heartbeat.js +9 -0
- package/build/executor/http-client.js +90 -22
- package/build/executor/job-errors.js +43 -2
- package/build/executor/job-runner.js +137 -29
- package/build/executor/merge-job.js +102 -6
- package/build/executor/permissions.js +106 -0
- package/build/executor/preflight.js +38 -13
- package/build/executor/resume-pre-spawn.js +2 -1
- package/build/executor/runner.js +175 -4
- package/build/executor/service-unit.js +15 -0
- package/build/executor/terminal-mutation.js +22 -1
- package/build/executor/types.js +86 -0
- package/build/executor/worker-command.js +21 -5
- package/build/executor/worker-guard-hook.js +939 -0
- package/build/executor/worker-log.js +56 -0
- package/build/executor/worktree.js +11 -0
- package/build/git-reachability.js +147 -0
- package/build/index.js +535 -95
- package/build/install-bridge.js +95 -0
- package/build/pipelines.generated.js +10 -2
- package/build/plan-epic-conductor-eligibility.js +213 -0
- package/build/plane/cli.js +78 -15
- package/build/plane/defaults.js +165 -0
- package/build/plane/manifest.js +63 -8
- package/build/plane/member-logs.js +6 -0
- package/build/plane/member-roster.js +195 -11
- package/build/plane/preflight.js +43 -0
- package/build/plane/shutdown.js +25 -3
- package/build/plane/status.js +11 -0
- package/build/plane/supervisor.js +343 -14
- package/build/plane/test-fakes.js +43 -0
- package/build/plane/types.js +82 -11
- package/build/pr-base-contract.js +20 -0
- package/build/readme.generated.js +1 -1
- package/build/review-synthesis-config.js +60 -0
- package/build/scripts/executor-protocol-contract-driver.js +311 -0
- package/build/setup-epic.js +592 -139
- package/build/sfcc/log-query.js +2 -1
- package/build/sfcc/reads-custom-object-def.js +10 -13
- package/build/sfcc/reads-site-preference.js +5 -5
- package/build/sfcc/reads-system-object.js +4 -4
- package/build/sfcc/writes-custom-object-def.js +7 -7
- package/build/sfcc/writes-site-preference.js +4 -3
- package/build/sfcc/writes-system-object.js +7 -6
- package/build/start-tickets-conductor.js +11 -2
- package/build/start-tickets.js +69 -2
- package/build/version.generated.js +3 -3
- package/build/worker-containment-diagnostic.js +97 -0
- package/build/worker-guard-hook-bin.js +6 -0
- package/docs/CONDUCTOR.md +27 -0
- package/docs/install/mcp-tool-integrations.md +3 -2
- package/package.json +5 -3
- package/pipelines/plan-epic.json +5 -0
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge API access + polling primitives for the sessionless Bitbucket connect flow
|
|
3
|
+
* (BAPI-966).
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `connect-github-api.ts`'s request contract, lifecycle vocabulary, and polling
|
|
6
|
+
* policy, adapted to the Bitbucket Forge backend (`api/routes/setup/bitbucket_connection_codes.py`):
|
|
7
|
+
* candidates are identified by an immutable `(workspace_uuid, repository_uuid)` pair
|
|
8
|
+
* rather than a single numeric id, and there is no delegated-handoff mode — BAPI-686 is
|
|
9
|
+
* GitHub-only.
|
|
10
|
+
*
|
|
11
|
+
* Two rules shape everything here, unchanged from the GitHub client:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Nothing secret is ever returned in a message.** Failures collapse to coarse,
|
|
14
|
+
* enumerated categories. Fetch exception text, response bodies, headers, the state
|
|
15
|
+
* nonce, the install URL, and the API key never reach a string a caller might print.
|
|
16
|
+
* 2. **A malformed success is a failure.** Responses are validated against explicit
|
|
17
|
+
* unions; a 200 whose body does not match is rejected rather than guessed at, so a
|
|
18
|
+
* server change can never be silently reinterpreted as a connection outcome.
|
|
19
|
+
*/
|
|
20
|
+
/** Statuses that end the flow — polling past one of these is pointless. */
|
|
21
|
+
const TERMINAL_STATUSES = new Set([
|
|
22
|
+
"staged",
|
|
23
|
+
"connected",
|
|
24
|
+
"no-repositories",
|
|
25
|
+
"expired",
|
|
26
|
+
"invalid",
|
|
27
|
+
]);
|
|
28
|
+
const ALL_STATUSES = new Set([
|
|
29
|
+
"waiting",
|
|
30
|
+
...TERMINAL_STATUSES,
|
|
31
|
+
]);
|
|
32
|
+
/** Per-request timeout. Generous enough for a cold server, short enough to retry. */
|
|
33
|
+
const REQUEST_TIMEOUT_MS = 15_000;
|
|
34
|
+
/**
|
|
35
|
+
* POST JSON to a Bridge endpoint.
|
|
36
|
+
*
|
|
37
|
+
* The state nonce travels in the BODY, never the query string: query strings land in
|
|
38
|
+
* server access logs, proxy logs, and browser history in a way request bodies do not.
|
|
39
|
+
*/
|
|
40
|
+
async function postJson(deps, path, payload) {
|
|
41
|
+
let resp;
|
|
42
|
+
try {
|
|
43
|
+
resp = await deps.fetch(`${deps.baseUrl}${path}`, {
|
|
44
|
+
method: "POST",
|
|
45
|
+
headers: {
|
|
46
|
+
"Content-Type": "application/json",
|
|
47
|
+
"X-API-Key": deps.apiKey,
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify(payload),
|
|
50
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
// Deliberately does not forward `e`: fetch exception text can echo the URL, which
|
|
55
|
+
// carries the state nonce.
|
|
56
|
+
const isTimeout = e instanceof Error && (e.name === "TimeoutError" || e.name === "AbortError");
|
|
57
|
+
return { ok: false, kind: isTimeout ? "timeout" : "network" };
|
|
58
|
+
}
|
|
59
|
+
let body = null;
|
|
60
|
+
try {
|
|
61
|
+
body = await resp.json();
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* Non-JSON body. The status still classifies the outcome. */
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
value: { status: resp.status, body, retryAfter: resp.headers.get("Retry-After") },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Map a non-2xx status to a coarse category. */
|
|
72
|
+
function classifyStatus(status) {
|
|
73
|
+
if (status === 401 || status === 403)
|
|
74
|
+
return "unauthorized";
|
|
75
|
+
if (status === 404)
|
|
76
|
+
return "not-found";
|
|
77
|
+
return "server";
|
|
78
|
+
}
|
|
79
|
+
function asRecord(value) {
|
|
80
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
81
|
+
? value
|
|
82
|
+
: null;
|
|
83
|
+
}
|
|
84
|
+
function asString(value) {
|
|
85
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
86
|
+
}
|
|
87
|
+
function asNullableString(value) {
|
|
88
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
89
|
+
}
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Retry-After
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
/**
|
|
94
|
+
* Parse a `Retry-After` header to milliseconds.
|
|
95
|
+
*
|
|
96
|
+
* Handles both RFC forms — delta-seconds and an HTTP-date — and clamps the result to
|
|
97
|
+
* `remainingMs` so a server (or a bogus far-future date) can never push a wait past the
|
|
98
|
+
* caller's own deadline. Returns null for absent/unparseable/negative values.
|
|
99
|
+
*/
|
|
100
|
+
export function parseRetryAfterMs(header, remainingMs, nowMs) {
|
|
101
|
+
if (!header)
|
|
102
|
+
return null;
|
|
103
|
+
const trimmed = header.trim();
|
|
104
|
+
if (!trimmed)
|
|
105
|
+
return null;
|
|
106
|
+
const clamp = (ms) => {
|
|
107
|
+
if (!Number.isFinite(ms) || ms < 0)
|
|
108
|
+
return null;
|
|
109
|
+
return Math.min(ms, Math.max(0, remainingMs));
|
|
110
|
+
};
|
|
111
|
+
// delta-seconds
|
|
112
|
+
if (/^\d+$/.test(trimmed)) {
|
|
113
|
+
return clamp(Number(trimmed) * 1_000);
|
|
114
|
+
}
|
|
115
|
+
// HTTP-date
|
|
116
|
+
const parsed = Date.parse(trimmed);
|
|
117
|
+
if (Number.isNaN(parsed))
|
|
118
|
+
return null;
|
|
119
|
+
return clamp(parsed - nowMs);
|
|
120
|
+
}
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Endpoints
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
/** Mint a CLI-origin connection code and its Forge install-page URL. */
|
|
125
|
+
export async function mintBitbucketConnection(deps, repoName) {
|
|
126
|
+
const res = await postJson(deps, "/setup/bitbucket/cli/connection-code", {
|
|
127
|
+
repo_name: repoName,
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok)
|
|
130
|
+
return res;
|
|
131
|
+
if (res.value.status !== 200) {
|
|
132
|
+
return { ok: false, kind: classifyStatus(res.value.status) };
|
|
133
|
+
}
|
|
134
|
+
const body = asRecord(res.value.body);
|
|
135
|
+
const state = asString(body?.state);
|
|
136
|
+
const installUrl = asString(body?.install_url);
|
|
137
|
+
const ttlSeconds = body?.ttl_seconds;
|
|
138
|
+
if (!body || !state || !installUrl || typeof ttlSeconds !== "number") {
|
|
139
|
+
return { ok: false, kind: "malformed" };
|
|
140
|
+
}
|
|
141
|
+
return { ok: true, value: { state, installUrl, ttlSeconds } };
|
|
142
|
+
}
|
|
143
|
+
/** Strictly parse candidates. A candidate missing a usable identity fails the whole list. */
|
|
144
|
+
function parseCandidates(value) {
|
|
145
|
+
if (!Array.isArray(value))
|
|
146
|
+
return null;
|
|
147
|
+
const out = [];
|
|
148
|
+
for (const raw of value) {
|
|
149
|
+
const rec = asRecord(raw);
|
|
150
|
+
const workspaceUuid = asString(rec?.workspace_uuid);
|
|
151
|
+
const repositoryUuid = asString(rec?.repository_uuid);
|
|
152
|
+
const workspaceSlug = asString(rec?.workspace_slug);
|
|
153
|
+
const repositorySlug = asString(rec?.repository_slug);
|
|
154
|
+
const defaultBranch = asString(rec?.default_branch);
|
|
155
|
+
// A candidate without every usable selection identifier is a malformed response,
|
|
156
|
+
// not a candidate to silently drop — dropping it would show an incomplete picker.
|
|
157
|
+
if (!rec || !workspaceUuid || !repositoryUuid || !workspaceSlug || !repositorySlug || !defaultBranch) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
out.push({
|
|
161
|
+
workspace_uuid: workspaceUuid,
|
|
162
|
+
repository_uuid: repositoryUuid,
|
|
163
|
+
workspace_slug: workspaceSlug,
|
|
164
|
+
repository_slug: repositorySlug,
|
|
165
|
+
display_name: asNullableString(rec.display_name),
|
|
166
|
+
default_branch: defaultBranch,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
/** Read one connection code's current lifecycle state. */
|
|
172
|
+
export async function fetchBitbucketConnectionStatus(deps, repoName, state, nowMs = Date.now()) {
|
|
173
|
+
const res = await postJson(deps, "/setup/bitbucket/cli/status", {
|
|
174
|
+
repo_name: repoName,
|
|
175
|
+
state,
|
|
176
|
+
});
|
|
177
|
+
if (!res.ok)
|
|
178
|
+
return res;
|
|
179
|
+
if (res.value.status !== 200) {
|
|
180
|
+
return { ok: false, kind: classifyStatus(res.value.status) };
|
|
181
|
+
}
|
|
182
|
+
const body = asRecord(res.value.body);
|
|
183
|
+
const status = asString(body?.status);
|
|
184
|
+
if (!body || !status || !ALL_STATUSES.has(status)) {
|
|
185
|
+
return { ok: false, kind: "malformed" };
|
|
186
|
+
}
|
|
187
|
+
const candidates = parseCandidates(body.candidates ?? []);
|
|
188
|
+
if (candidates === null)
|
|
189
|
+
return { ok: false, kind: "malformed" };
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
value: {
|
|
193
|
+
status: status,
|
|
194
|
+
candidates,
|
|
195
|
+
repoDisplayName: asNullableString(body.repo_display_name),
|
|
196
|
+
// A single read has no deadline of its own to clamp against; the poller applies
|
|
197
|
+
// its own bound. POLL_DEADLINE_MS is the widest a caller could honor.
|
|
198
|
+
retryAfterMs: parseRetryAfterMs(res.value.retryAfter, POLL_DEADLINE_MS, nowMs),
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** Bind one chosen repository. Sends no installation id — the server owns that. */
|
|
203
|
+
export async function confirmBitbucketConnection(deps, repoName, state, workspaceUuid, repositoryUuid) {
|
|
204
|
+
const res = await postJson(deps, "/setup/bitbucket/cli/confirm", {
|
|
205
|
+
repo_name: repoName,
|
|
206
|
+
state,
|
|
207
|
+
workspace_uuid: workspaceUuid,
|
|
208
|
+
repository_uuid: repositoryUuid,
|
|
209
|
+
});
|
|
210
|
+
if (!res.ok)
|
|
211
|
+
return res;
|
|
212
|
+
if (res.value.status !== 200) {
|
|
213
|
+
return { ok: false, kind: classifyStatus(res.value.status) };
|
|
214
|
+
}
|
|
215
|
+
const body = asRecord(res.value.body);
|
|
216
|
+
const repoName_ = asString(body?.repo_name);
|
|
217
|
+
if (!body || body.status !== "connected" || !repoName_) {
|
|
218
|
+
return { ok: false, kind: "malformed" };
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
ok: true,
|
|
222
|
+
value: {
|
|
223
|
+
repoName: repoName_,
|
|
224
|
+
workspaceSlug: asNullableString(body.workspace_slug),
|
|
225
|
+
repositorySlug: asNullableString(body.repository_slug),
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Read whether Bitbucket is already configured for a project.
|
|
231
|
+
*
|
|
232
|
+
* Uses the existing authenticated install-manifest/capability surface — read-only, and
|
|
233
|
+
* unrelated to any in-flight connection attempt. `unavailable` is a real answer and is
|
|
234
|
+
* deliberately distinct from `unconfigured`: callers must be able to skip rather than
|
|
235
|
+
* fabricate "not configured" from a probe that simply failed.
|
|
236
|
+
*/
|
|
237
|
+
export async function fetchBitbucketConfigurationState(deps, repoName) {
|
|
238
|
+
let resp;
|
|
239
|
+
try {
|
|
240
|
+
resp = await deps.fetch(`${deps.baseUrl}/jira/config/install-manifest?repo_name=${encodeURIComponent(repoName)}`, {
|
|
241
|
+
headers: { "X-API-Key": deps.apiKey },
|
|
242
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return "unavailable";
|
|
247
|
+
}
|
|
248
|
+
if (resp.status !== 200)
|
|
249
|
+
return "unavailable";
|
|
250
|
+
let body;
|
|
251
|
+
try {
|
|
252
|
+
body = await resp.json();
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return "unavailable";
|
|
256
|
+
}
|
|
257
|
+
const integrations = asRecord(body)?.integrations;
|
|
258
|
+
if (!Array.isArray(integrations))
|
|
259
|
+
return "unavailable";
|
|
260
|
+
for (const raw of integrations) {
|
|
261
|
+
const rec = asRecord(raw);
|
|
262
|
+
if (rec?.id !== "vcs_access_token")
|
|
263
|
+
continue;
|
|
264
|
+
if (typeof rec.is_configured !== "boolean")
|
|
265
|
+
return "unavailable";
|
|
266
|
+
return rec.is_configured ? "configured" : "unconfigured";
|
|
267
|
+
}
|
|
268
|
+
// The checklist is provider-scoped: this descriptor is legitimately absent for a
|
|
269
|
+
// GitHub project. Absent is not "unconfigured" — there is nothing here to connect.
|
|
270
|
+
return "unavailable";
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Slightly longer than the server's 15-minute code TTL, so an expiry is reported by the
|
|
274
|
+
* server as `expired` rather than guessed at by a local timeout.
|
|
275
|
+
*/
|
|
276
|
+
export const POLL_DEADLINE_MS = 15.5 * 60 * 1_000;
|
|
277
|
+
/** Back off gently: quick early (the common case is fast), then settle. */
|
|
278
|
+
const POLL_DELAYS_MS = [2_000, 3_000, 5_000];
|
|
279
|
+
const MAX_JITTER_MS = 400;
|
|
280
|
+
/**
|
|
281
|
+
* TRANSPORT faults worth retrying — the request never produced a response at all.
|
|
282
|
+
*
|
|
283
|
+
* Deliberately does NOT list "server": `postJson` only ever reports these two kinds,
|
|
284
|
+
* because any HTTP response (500 included) comes back as `ok: true` with a status. HTTP
|
|
285
|
+
* statuses are classified by {@link isRetryableStatus} instead.
|
|
286
|
+
*/
|
|
287
|
+
const RETRYABLE_TRANSPORT = new Set([
|
|
288
|
+
"network",
|
|
289
|
+
"timeout",
|
|
290
|
+
]);
|
|
291
|
+
/**
|
|
292
|
+
* Is this HTTP status worth retrying rather than surfacing?
|
|
293
|
+
*
|
|
294
|
+
* Every 5xx, not just 502/503: a transient server-side hiccup and a 504 gateway timeout
|
|
295
|
+
* are exactly as transient as a dropped packet, and the user's alternative is restarting
|
|
296
|
+
* the whole mint → browser → poll flow. 429 is retried because it is literally a request
|
|
297
|
+
* to retry. 4xx is terminal: the request itself is the problem, so repeating it cannot help.
|
|
298
|
+
*/
|
|
299
|
+
function isRetryableStatus(status) {
|
|
300
|
+
return status === 429 || status >= 500;
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Poll until a terminal status, an unrecoverable failure, or the deadline.
|
|
304
|
+
*
|
|
305
|
+
* Terminal statuses return immediately — including the failure ones. Waiting out a
|
|
306
|
+
* 15-minute deadline on a code the server already called `expired` would be theatre.
|
|
307
|
+
*
|
|
308
|
+
* Transient transport faults (network blips, timeouts, 502/503) are retried, because a
|
|
309
|
+
* dropped packet is not a failed connection. `Retry-After` is honored when the server
|
|
310
|
+
* sends it, including on a 429.
|
|
311
|
+
*/
|
|
312
|
+
export async function pollBitbucketConnection(deps, poll, repoName, state) {
|
|
313
|
+
const started = poll.now();
|
|
314
|
+
let attempt = 0;
|
|
315
|
+
for (;;) {
|
|
316
|
+
const elapsed = poll.now() - started;
|
|
317
|
+
const remaining = POLL_DEADLINE_MS - elapsed;
|
|
318
|
+
if (remaining <= 0)
|
|
319
|
+
return { ok: false, kind: "deadline" };
|
|
320
|
+
const res = await postJson(deps, "/setup/bitbucket/cli/status", {
|
|
321
|
+
repo_name: repoName,
|
|
322
|
+
state,
|
|
323
|
+
});
|
|
324
|
+
let waitMs = null;
|
|
325
|
+
if (!res.ok) {
|
|
326
|
+
if (!RETRYABLE_TRANSPORT.has(res.kind))
|
|
327
|
+
return { ok: false, kind: res.kind };
|
|
328
|
+
}
|
|
329
|
+
else if (res.value.status === 200) {
|
|
330
|
+
const body = asRecord(res.value.body);
|
|
331
|
+
const status = asString(body?.status);
|
|
332
|
+
if (!body || !status || !ALL_STATUSES.has(status)) {
|
|
333
|
+
return { ok: false, kind: "malformed" };
|
|
334
|
+
}
|
|
335
|
+
const candidates = parseCandidates(body.candidates ?? []);
|
|
336
|
+
if (candidates === null)
|
|
337
|
+
return { ok: false, kind: "malformed" };
|
|
338
|
+
const typed = status;
|
|
339
|
+
if (TERMINAL_STATUSES.has(typed)) {
|
|
340
|
+
return {
|
|
341
|
+
ok: true,
|
|
342
|
+
value: {
|
|
343
|
+
status: typed,
|
|
344
|
+
candidates,
|
|
345
|
+
repoDisplayName: asNullableString(body.repo_display_name),
|
|
346
|
+
retryAfterMs: null,
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
// `waiting` — honor server pacing if offered.
|
|
351
|
+
waitMs = parseRetryAfterMs(res.value.retryAfter, remaining, poll.now());
|
|
352
|
+
}
|
|
353
|
+
else if (isRetryableStatus(res.value.status)) {
|
|
354
|
+
waitMs = parseRetryAfterMs(res.value.retryAfter, remaining, poll.now());
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
return { ok: false, kind: classifyStatus(res.value.status) };
|
|
358
|
+
}
|
|
359
|
+
if (waitMs === null) {
|
|
360
|
+
const base = POLL_DELAYS_MS[Math.min(attempt, POLL_DELAYS_MS.length - 1)];
|
|
361
|
+
waitMs = base + Math.floor(poll.jitter() * MAX_JITTER_MS);
|
|
362
|
+
}
|
|
363
|
+
attempt += 1;
|
|
364
|
+
// Never sleep past the deadline.
|
|
365
|
+
const capped = Math.min(waitMs, Math.max(0, POLL_DEADLINE_MS - (poll.now() - started)));
|
|
366
|
+
if (capped <= 0)
|
|
367
|
+
return { ok: false, kind: "deadline" };
|
|
368
|
+
await poll.sleep(capped);
|
|
369
|
+
}
|
|
370
|
+
}
|