{"version":3,"file":"capacity-tracker.mjs","names":[],"sources":["../src/capacity-tracker.ts"],"sourcesContent":["/**\n * Client-side estimate of the organization's queue capacity, driven by the\n * `X-Task-Priority` / `X-P1-Capacity` / `X-P2-Capacity` / `X-P3-Capacity`\n * headers that pipe0's run-creating endpoints return on every response.\n *\n * The server never rejects below the hard ceiling — submissions just enter at\n * a worse priority — so pacing must be capacity-led rather than error-led:\n * the last create's headers are the authoritative baseline, records the SDK\n * observes completing (via its own polls or cancels) are credited back, and\n * any drift from other clients consuming capacity self-corrects at the next\n * create response.\n */\n\n/** Estimated remaining records per priority window (P1 ≤ P2 ≤ P3). */\nexport interface CapacitySnapshot {\n  /** Priority the most recently observed run was admitted at. */\n  taskPriority: 1 | 2 | 3;\n  /** Records that can still be submitted at priority 1. */\n  p1: number;\n  /** Records that can still be submitted at priority 2 or better. */\n  p2: number;\n  /** Records that can still be submitted at all — at 0, submissions 429. */\n  p3: number;\n  /** When the underlying headers were observed (ms epoch). */\n  observedAt: number;\n}\n\nconst MAX_TRACKED_RUNS = 1000;\n\nexport class CapacityTracker {\n  /** Last authoritative header observation; null until the first create. */\n  private base: CapacitySnapshot | null = null;\n  /** Records of creates issued whose response has not settled yet. */\n  private pending = 0;\n  /** Records credited back (completions/cancels) since `base` was observed. */\n  private credited = 0;\n  /** runId → records, for crediting terminal runs exactly once. */\n  private runRecords = new Map<string, number>();\n\n  /**\n   * Re-baseline from a response's headers. Returns the adjusted snapshot, or\n   * null when the response carries no capacity headers (e.g. an\n   * `org-rate-limited` 429 emitted by middleware before the handler ran).\n   */\n  observeHeaders(headers: Headers): CapacitySnapshot | null {\n    if (headers.get(\"X-P3-Capacity\") === null) return null;\n    const num = (name: string) => {\n      const value = Number(headers.get(name));\n      return Number.isFinite(value) ? Math.max(0, value) : 0;\n    };\n    const priority = Number(headers.get(\"X-Task-Priority\"));\n    this.base = {\n      taskPriority: priority === 2 || priority === 3 ? priority : 1,\n      p1: num(\"X-P1-Capacity\"),\n      p2: num(\"X-P2-Capacity\"),\n      p3: num(\"X-P3-Capacity\"),\n      observedAt: Date.now(),\n    };\n    // The new baseline already reflects everything the server has seen.\n    this.credited = 0;\n    return this.snapshot();\n  }\n\n  /** Call synchronously BEFORE issuing a create — prevents two concurrent submitters from double-spending the same estimate. */\n  onCreateIssued(records: number): void {\n    this.pending += records;\n  }\n\n  /** Call after the create response settled (the middleware has re-baselined by then). */\n  onCreateSettled(records: number): void {\n    this.pending = Math.max(0, this.pending - records);\n  }\n\n  /** Remember a run's record weight so its completion can be credited back. */\n  registerRun(runId: string, records: number): void {\n    this.runRecords.set(runId, records);\n    if (this.runRecords.size > MAX_TRACKED_RUNS) {\n      const oldest = this.runRecords.keys().next().value;\n      if (oldest !== undefined) this.runRecords.delete(oldest);\n    }\n  }\n\n  /**\n   * Credit a run's records back to every window estimate. The Map delete\n   * makes double-crediting (cancel + a later poll observing \"canceled\")\n   * impossible. Returns the credited record count (0 if unknown/already credited).\n   */\n  creditTerminal(runId: string): number {\n    const records = this.runRecords.get(runId);\n    if (records === undefined) return 0;\n    this.runRecords.delete(runId);\n    this.credited += records;\n    return records;\n  }\n\n  /**\n   * Estimated remaining records in the given priority window. Null when no\n   * create has been observed yet — callers treat that as \"gate open\".\n   */\n  estimate(floor: 1 | 2 | 3): number | null {\n    if (this.base === null) return null;\n    const window = floor === 1 ? this.base.p1 : floor === 2 ? this.base.p2 : this.base.p3;\n    return Math.max(0, window - this.pending + this.credited);\n  }\n\n  /** The current adjusted snapshot, or null before the first observation. */\n  snapshot(): CapacitySnapshot | null {\n    if (this.base === null) return null;\n    const adjust = (value: number) => Math.max(0, value - this.pending + this.credited);\n    return {\n      taskPriority: this.base.taskPriority,\n      p1: adjust(this.base.p1),\n      p2: adjust(this.base.p2),\n      p3: adjust(this.base.p3),\n      observedAt: this.base.observedAt,\n    };\n  }\n}\n"],"mappings":";AA2BA,MAAM,mBAAmB;AAEzB,IAAa,kBAAb,MAA6B;;CAE3B,AAAQ,OAAgC;;CAExC,AAAQ,UAAU;;CAElB,AAAQ,WAAW;;CAEnB,AAAQ,6BAAa,IAAI,KAAqB;;;;;;CAO9C,eAAe,SAA2C;AACxD,MAAI,QAAQ,IAAI,gBAAgB,KAAK,KAAM,QAAO;EAClD,MAAM,OAAO,SAAiB;GAC5B,MAAM,QAAQ,OAAO,QAAQ,IAAI,KAAK,CAAC;AACvC,UAAO,OAAO,SAAS,MAAM,GAAG,KAAK,IAAI,GAAG,MAAM,GAAG;;EAEvD,MAAM,WAAW,OAAO,QAAQ,IAAI,kBAAkB,CAAC;AACvD,OAAK,OAAO;GACV,cAAc,aAAa,KAAK,aAAa,IAAI,WAAW;GAC5D,IAAI,IAAI,gBAAgB;GACxB,IAAI,IAAI,gBAAgB;GACxB,IAAI,IAAI,gBAAgB;GACxB,YAAY,KAAK,KAAK;GACvB;AAED,OAAK,WAAW;AAChB,SAAO,KAAK,UAAU;;;CAIxB,eAAe,SAAuB;AACpC,OAAK,WAAW;;;CAIlB,gBAAgB,SAAuB;AACrC,OAAK,UAAU,KAAK,IAAI,GAAG,KAAK,UAAU,QAAQ;;;CAIpD,YAAY,OAAe,SAAuB;AAChD,OAAK,WAAW,IAAI,OAAO,QAAQ;AACnC,MAAI,KAAK,WAAW,OAAO,kBAAkB;GAC3C,MAAM,SAAS,KAAK,WAAW,MAAM,CAAC,MAAM,CAAC;AAC7C,OAAI,WAAW,OAAW,MAAK,WAAW,OAAO,OAAO;;;;;;;;CAS5D,eAAe,OAAuB;EACpC,MAAM,UAAU,KAAK,WAAW,IAAI,MAAM;AAC1C,MAAI,YAAY,OAAW,QAAO;AAClC,OAAK,WAAW,OAAO,MAAM;AAC7B,OAAK,YAAY;AACjB,SAAO;;;;;;CAOT,SAAS,OAAiC;AACxC,MAAI,KAAK,SAAS,KAAM,QAAO;EAC/B,MAAM,SAAS,UAAU,IAAI,KAAK,KAAK,KAAK,UAAU,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK;AACnF,SAAO,KAAK,IAAI,GAAG,SAAS,KAAK,UAAU,KAAK,SAAS;;;CAI3D,WAAoC;AAClC,MAAI,KAAK,SAAS,KAAM,QAAO;EAC/B,MAAM,UAAU,UAAkB,KAAK,IAAI,GAAG,QAAQ,KAAK,UAAU,KAAK,SAAS;AACnF,SAAO;GACL,cAAc,KAAK,KAAK;GACxB,IAAI,OAAO,KAAK,KAAK,GAAG;GACxB,IAAI,OAAO,KAAK,KAAK,GAAG;GACxB,IAAI,OAAO,KAAK,KAAK,GAAG;GACxB,YAAY,KAAK,KAAK;GACvB"}