mirror of
https://github.com/stablyai/orca.git
synced 2026-10-14 00:02:42 +00:00
Gate gh retries on idempotency (auto-detected from argv/-X/mutation), key parent-field probe by (owner,ownerType), accept 'to an' in not_found classifier, and replace bulk label PUT with parallel sing Co-authored-by: orca-bug-scan-bot <orca-bug-scan-bot@stably.ai>
1540 lines
53 KiB
TypeScript
1540 lines
53 KiB
TypeScript
/* eslint-disable max-lines -- Why: ProjectV2 GraphQL has its own normalization
|
||
layer, retry policy (parent-field dance), paste-to-add parser, and discovery
|
||
pagination. Co-locating the read path keeps the retry/classify/normalize
|
||
contract reviewable as one surface. Lower-level plumbing (slug validation,
|
||
error classifier, runGraphql/runRest) lives in ./project-view/internals; the
|
||
slug-addressed write path lives in ./project-view/mutations. */
|
||
import {
|
||
acquire,
|
||
release,
|
||
extractExecError,
|
||
ghExecFileAsync,
|
||
rateLimitGuard,
|
||
noteRateLimitSpend,
|
||
classifyProjectError,
|
||
driftError,
|
||
errorsIndicateParentField,
|
||
rateLimitedError,
|
||
runGraphql,
|
||
isValidOwnerSlug,
|
||
assertSlug,
|
||
assertPositiveInt,
|
||
type GhGraphqlErrorShape,
|
||
type GraphqlVars
|
||
} from './project-view/internals'
|
||
import type {
|
||
GetProjectViewTableArgs,
|
||
GetProjectViewTableResult,
|
||
GitHubProjectField,
|
||
GitHubProjectFieldValue,
|
||
GitHubProjectIteration,
|
||
GitHubProjectLabel,
|
||
GitHubProjectOwnerType,
|
||
GitHubProjectRow,
|
||
GitHubProjectRowItemType,
|
||
GitHubProjectSingleSelectOption,
|
||
GitHubProjectSort,
|
||
GitHubProjectSummary,
|
||
GitHubProjectTable,
|
||
GitHubProjectUser,
|
||
GitHubProjectView,
|
||
GitHubProjectViewError,
|
||
GitHubProjectViewLayout,
|
||
GitHubProjectViewSummary,
|
||
ListAccessibleProjectsResult,
|
||
ListProjectViewsArgs,
|
||
ListProjectViewsResult,
|
||
ResolveProjectRefArgs,
|
||
ResolveProjectRefResult
|
||
} from '../../shared/github-project-types'
|
||
|
||
// Re-export the public API so existing call sites (`./project-view`) keep
|
||
// working unchanged. The split is internal-only.
|
||
export {
|
||
isValidOwnerSlug,
|
||
isValidRepoSlug,
|
||
isValidSlug,
|
||
classifyProjectError
|
||
} from './project-view/internals'
|
||
export {
|
||
updateProjectItemFieldValue,
|
||
clearProjectItemFieldValue,
|
||
updateIssueBySlug,
|
||
updatePullRequestBySlug,
|
||
addIssueCommentBySlug,
|
||
updateIssueCommentBySlug,
|
||
deleteIssueCommentBySlug,
|
||
listLabelsBySlug,
|
||
listAssignableUsersBySlug,
|
||
listIssueTypesBySlug,
|
||
updateIssueTypeBySlug,
|
||
getWorkItemDetailsBySlug
|
||
} from './project-view/mutations'
|
||
|
||
// ─── Constants ─────────────────────────────────────────────────────────
|
||
|
||
// Why: these defaults were deliberately shrunk from 50/50/100 to cut quota
|
||
// spend in the most expensive gh call reachable from TaskPage. Discovery
|
||
// walks viewer projects, then up to `DISCOVERY_MAX_ORGS` orgs × a nested
|
||
// `projectsV2(first:N)` query. The org loop dominates the cost and is the
|
||
// path that produced the user-visible HTTP 504 when one org was slow. Users
|
||
// with projects outside this window can still paste a URL to reach them —
|
||
// no functional loss. Prior values: MAX_ORGS=50, ORG_PAGE_SIZE=30,
|
||
// PROJECTS_PER_OWNER=100, nested projectsV2 first=50.
|
||
const ITEM_PAGE_SIZE = 100
|
||
const MAX_ITEMS = 500
|
||
const VIEWS_PAGE_SIZE = 20
|
||
const FIELDS_PAGE_SIZE = 50
|
||
const DISCOVERY_PROJECTS_PER_OWNER = 40
|
||
const DISCOVERY_MAX_ORGS = 20
|
||
const DISCOVERY_ORG_PAGE_SIZE = 20
|
||
const DISCOVERY_PROJECTS_PER_ORG = 20
|
||
const FIELD_VALUES_PAGE_SIZE = 100
|
||
|
||
// ─── Module-scope caches (reset on HMR — intentional) ──────────────────
|
||
|
||
// Why: HMR reloading should re-probe capability. Both caches live as plain
|
||
// module locals so a dev-time code swap naturally re-runs capability probes
|
||
// instead of carrying a stale "unsupported" flag into fresh code.
|
||
const ownerTypeCache = new Map<string, GitHubProjectOwnerType | null>()
|
||
// Why: keyed by `${owner}\0${ownerType}` rather than a process-global flag so
|
||
// a single owner's capability gap (e.g. token without scope on org A) doesn't
|
||
// poison every subsequent fetch for unrelated owners that DO support
|
||
// Issue.parent. See bug-scan finding 2.
|
||
const parentFieldRetriedByOwner = new Set<string>()
|
||
const parentFieldWarningLoggedByOwner = new Set<string>()
|
||
// Why: concurrent fetchAllItems calls for the same owner all observe the
|
||
// same "not-yet-retried" state, each issuing a duplicate first-page probe
|
||
// and racing to set the flag. Use an in-flight promise per owner so only
|
||
// one caller drives the probe; siblings await the same result.
|
||
const parentFieldProbeInFlight = new Map<string, Promise<void>>()
|
||
|
||
function ownerScopeKey(owner: string, ownerType: GitHubProjectOwnerType): string {
|
||
return `${owner} |