/** * Copyright (c) 2026, Salesforce, Inc., * All rights reserved. * For full license text, see the LICENSE.txt file */ import { type OrgAuth } from "./auth.js"; import { type RetryHint } from "./errors.js"; import { type SchemaMetadata } from "./introspect.js"; /** * Acquire a filesystem advisory lock at `${finalPath}.lock` (a directory, * since `mkdir` is atomic on POSIX), run `work`, then release the lock. * * If another process holds the lock, this polls every 100ms for the lock * to be released. On release, if the work is already satisfied (per `skipIf`), * the work function is skipped (the other process already did it). Otherwise * this acquires the lock and runs `work`. * * Contract: `work` will NOT run when `skipIf()` is true at any point during * the lock dance — including immediately after we acquire the lock (a holder * may have finished and released the lock between our last EEXIST poll and * our successful `mkdir`). In that case the lock is released and the * function returns `undefined`. * * `skipIf` defaults to "`finalPath` exists" — the lazy-prime contract (any * cached schema is good enough). A forced refresh passes a predicate that is * true only once the file has been *replaced* since the refresh was requested, * which coalesces concurrent refreshes onto a single introspection. * * Stale locks (older than STALE_LOCK_MS) are reclaimed. */ export declare function withSchemaLock(finalPath: string, work: () => Promise, opts?: { skipIf?: () => boolean; }): Promise; export interface PrimeResult { cached: boolean; /** True only when this call performed a forced re-download (forceRefresh). */ refreshed: boolean; filePath: string; durationMs: number; /** Resolved instance URL (already auth-resolved). */ instanceUrl: string; /** * Non-`__` type count from the just-downloaded schema. Present only when this * call performed a download (`cached: false`); a cache hit or coalesced * refresh leaves it undefined to avoid re-parsing the (large) introspection * JSON just to count types. */ typeCount?: number; } /** * Thrown when a forced refresh fails to download a new schema. The previously * cached schema (if any) is left intact on disk and in memory — we only clear * the caches *after* a successful download. `staleSince` is the surviving * cache's download timestamp (ISO) so callers can render a staleness-aware * message; it is undefined when no prior cache existed (a true hard failure) — * or when a cache file is present but unparseable, since `getSchemaMetadata` * returns null for a corrupt file, which correctly degrades to a hard failure * (a corrupt cache is not usable). `instanceUrl` is always the resolved org URL * (known before the download). * * Carries a {@link RetryHint} (`retry`, default `"no"`) for the MCP error surface * (W-23148365), derived from the underlying cause via {@link classifyCause}. Note * the surviving-cache case never reaches the MCP token surface: `buildConnect` * intercepts a `SchemaRefreshError` with a defined `staleSince` (i.e. a cache * survived) and returns a soft `warnings[]` success before `runTool` can stamp a * token — so `retry` is only ever observed for the no-surviving-cache hard failure. */ export declare class SchemaRefreshError extends Error { staleSince?: string; instanceUrl: string; readonly retry: RetryHint; constructor(message: string, opts: { instanceUrl: string; staleSince?: string; cause?: unknown; retry?: RetryHint; }); } /** * Dependencies that `primeSchemaWithLock` calls. Defaulted to the real * graphiti implementations; tests pass stubs. */ export interface PrimeDeps { getOrgAuth: (orgAlias: string) => Promise; downloadSchema: (auth: OrgAuth) => Promise; } /** * Lazily prime — or, with `forceRefresh`, re-download — the on-disk schema * cache for `orgAlias`. * * Lazy prime (`forceRefresh` falsy): * - If `/.graphiti/schemas/.json` already exists, returns * `{cached: true, refreshed: false}` immediately. * - Otherwise: resolves auth via `deps.getOrgAuth` (throws verbatim if the * alias is not authenticated, per FR-13.5), acquires the schema lock via * `withSchemaLock`, calls `deps.downloadSchema(auth)` (which writes the * cache atomically), releases the lock, and returns `{cached: false}`. Per * FR-13.6, partial caches never reach disk because writes are atomic. * `downloadSchema` bounds the request with a timeout and retries transient * failures (network / 5xx) once at the connection layer; deterministic * failures (4xx, GraphQL errors in a 200 body, missing `__schema`) and missing * auth surface immediately. Unlike a forced refresh, a lazy-prime failure is * re-thrown verbatim (there is no prior cache to keep, so no `SchemaRefreshError`). * * Forced refresh (`forceRefresh: true`) — W-22845606: * - Re-downloads even though the file exists, then clears all three caches * coherently: the on-disk introspection JSON (overwritten atomically), this * process's in-memory parsed-schema cache, and the ObjectInfo cache (memory * + disk). Clearing happens only AFTER a successful download, so a failed * refresh leaves every cache intact. * - Coalesces concurrent refreshes (CLI + MCP) into a single introspection via * a request-time mtime snapshot: a peer that replaced the file since this * refresh was requested satisfies us, and we skip the redundant download. * - The download retries transient failures (network / 5xx) once at the * connection layer. On terminal failure throws {@link SchemaRefreshError} * carrying the surviving cache's age; the old caches are kept. * * Concurrent callers in the same or different processes serialize on the lock * dir; only one performs introspection. */ export declare function primeSchemaWithLock(orgAlias: string, deps?: PrimeDeps, opts?: { forceRefresh?: boolean; }): Promise;