baa-conductor


baa-conductor / apps / conductor-daemon / src
codex@macbookpro  ·  2026-04-01

browser-request-policy.ts

   1import type { ArtifactStore } from "../../../packages/artifact-db/dist/index.js";
   2
   3export type BrowserRequestCircuitState = "closed" | "half_open" | "open";
   4export type BrowserRequestLeaseStatus = "cancelled" | "failure" | "success";
   5
   6type TimeoutHandle = ReturnType<typeof globalThis.setTimeout>;
   7
   8export interface BrowserRequestTarget {
   9  clientId: string;
  10  platform: string;
  11}
  12
  13export interface BrowserRequestStreamPolicyConfig {
  14  idleTimeoutMs: number;
  15  maxBufferedBytes: number;
  16  maxBufferedEvents: number;
  17  openTimeoutMs: number;
  18}
  19
  20export interface BrowserRequestStaleLeaseConfig {
  21  idleMs: number;
  22  sweepIntervalMs: number;
  23}
  24
  25export interface BrowserRequestPolicyConfig {
  26  backoff: {
  27    baseMs: number;
  28    maxMs: number;
  29  };
  30  circuitBreaker: {
  31    failureThreshold: number;
  32    openMs: number;
  33  };
  34  concurrency: {
  35    maxInFlightPerClientPlatform: number;
  36  };
  37  jitter: {
  38    maxMs: number;
  39    minMs: number;
  40    muMs: number;
  41    sigmaMs: number;
  42  };
  43  rateLimit: {
  44    requestsPerMinutePerPlatform: number;
  45    windowMs: number;
  46  };
  47  staleLease: BrowserRequestStaleLeaseConfig;
  48  stream: BrowserRequestStreamPolicyConfig;
  49}
  50
  51export interface BrowserRequestLeaseOutcome {
  52  code?: string | null;
  53  message?: string | null;
  54  status: BrowserRequestLeaseStatus;
  55}
  56
  57export interface BrowserRequestAdmission {
  58  admittedAt: number;
  59  backoffDelayMs: number;
  60  circuitState: BrowserRequestCircuitState;
  61  jitterDelayMs: number;
  62  platform: string;
  63  queueDelayMs: number;
  64  rateLimitDelayMs: number;
  65  requestId: string;
  66  requestedAt: number;
  67  targetClientId: string;
  68}
  69
  70export interface BrowserRequestPolicySnapshot {
  71  defaults: BrowserRequestPolicyConfig;
  72  platforms: Array<{
  73    lastDispatchedAt: number | null;
  74    platform: string;
  75    recentDispatchCount: number;
  76    waiting: number;
  77  }>;
  78  targets: Array<{
  79    backoffUntil: number | null;
  80    circuitRetryAt: number | null;
  81    circuitState: BrowserRequestCircuitState;
  82    clientId: string;
  83    consecutiveFailures: number;
  84    inFlight: number;
  85    lastActivityAt: number | null;
  86    lastActivityReason: string | null;
  87    lastError: string | null;
  88    lastFailureAt: number | null;
  89    lastStaleSweepAt: number | null;
  90    lastStaleSweepIdleMs: number | null;
  91    lastStaleSweepReason: string | null;
  92    lastStaleSweepRequestId: string | null;
  93    lastSuccessAt: number | null;
  94    platform: string;
  95    staleSweepCount: number;
  96    waiting: number;
  97  }>;
  98}
  99
 100export interface BrowserRequestPolicyPersistentPlatformState {
 101  dispatches: number[];
 102  lastDispatchedAt: number | null;
 103  platform: string;
 104}
 105
 106export interface BrowserRequestPolicyPersistentTargetState {
 107  backoffUntil: number | null;
 108  circuitRetryAt: number | null;
 109  circuitState: BrowserRequestCircuitState;
 110  clientId: string;
 111  consecutiveFailures: number;
 112  lastActivityAt: number | null;
 113  lastActivityReason: string | null;
 114  lastError: string | null;
 115  lastFailureAt: number | null;
 116  lastStaleSweepAt: number | null;
 117  lastStaleSweepIdleMs: number | null;
 118  lastStaleSweepReason: string | null;
 119  lastStaleSweepRequestId: string | null;
 120  lastSuccessAt: number | null;
 121  platform: string;
 122  staleSweepCount: number;
 123}
 124
 125export interface BrowserRequestPolicyPersistentState {
 126  platforms: BrowserRequestPolicyPersistentPlatformState[];
 127  targets: BrowserRequestPolicyPersistentTargetState[];
 128  version: 1;
 129}
 130
 131export interface BrowserRequestPolicyPersistence {
 132  load(): Promise<BrowserRequestPolicyPersistentState | null>;
 133  save(state: BrowserRequestPolicyPersistentState): Promise<void>;
 134}
 135
 136export interface BrowserRequestPolicyLease {
 137  readonly admission: BrowserRequestAdmission;
 138  readonly target: BrowserRequestTarget;
 139  complete(outcome: BrowserRequestLeaseOutcome): void;
 140  touch(reason?: string): void;
 141}
 142
 143export interface BrowserRequestPolicyControllerOptions {
 144  clearTimeoutImpl?: (handle: TimeoutHandle) => void;
 145  config?: Partial<BrowserRequestPolicyConfig>;
 146  now?: () => number;
 147  persistence?: BrowserRequestPolicyPersistence | null;
 148  persistenceDebounceMs?: number;
 149  random?: () => number;
 150  setTimeoutImpl?: (handler: () => void, timeoutMs: number) => TimeoutHandle;
 151}
 152
 153interface BrowserRequestPlatformState {
 154  busy: boolean;
 155  dispatches: number[];
 156  lastDispatchedAt: number | null;
 157  waiters: Array<() => void>;
 158}
 159
 160interface BrowserRequestLeaseRuntimeState {
 161  admittedAt: number | null;
 162  createdAt: number;
 163  lastActivityAt: number;
 164  lastActivityReason: string;
 165  requestId: string;
 166}
 167
 168interface BrowserRequestTargetState {
 169  backoffUntil: number | null;
 170  circuitRetryAt: number | null;
 171  circuitState: BrowserRequestCircuitState;
 172  consecutiveFailures: number;
 173  inFlight: number;
 174  lastActivityAt: number | null;
 175  lastActivityReason: string | null;
 176  lastError: string | null;
 177  lastFailureAt: number | null;
 178  lastStaleSweepAt: number | null;
 179  lastStaleSweepIdleMs: number | null;
 180  lastStaleSweepReason: string | null;
 181  lastStaleSweepRequestId: string | null;
 182  lastSuccessAt: number | null;
 183  leases: Map<string, BrowserRequestLeaseRuntimeState>;
 184  staleSweepCount: number;
 185  waiters: Array<() => void>;
 186}
 187
 188const DEFAULT_BROWSER_REQUEST_POLICY: BrowserRequestPolicyConfig = {
 189  backoff: {
 190    baseMs: 1_000,
 191    maxMs: 60_000
 192  },
 193  circuitBreaker: {
 194    failureThreshold: 5,
 195    openMs: 60_000
 196  },
 197  concurrency: {
 198    maxInFlightPerClientPlatform: 1
 199  },
 200  jitter: {
 201    maxMs: 5_000,
 202    minMs: 1_000,
 203    muMs: 2_000,
 204    sigmaMs: 500
 205  },
 206  rateLimit: {
 207    requestsPerMinutePerPlatform: 10,
 208    windowMs: 60_000
 209  },
 210  staleLease: {
 211    idleMs: 300_000,
 212    sweepIntervalMs: 30_000
 213  },
 214  stream: {
 215    idleTimeoutMs: 30_000,
 216    maxBufferedBytes: 512 * 1024,
 217    maxBufferedEvents: 256,
 218    openTimeoutMs: 10_000
 219  }
 220};
 221
 222const BROWSER_REQUEST_WAITER_TIMEOUT_MS = 120_000;
 223const DEFAULT_BROWSER_REQUEST_POLICY_PERSISTENCE_DEBOUNCE_MS = 25;
 224const DEFAULT_BROWSER_REQUEST_POLICY_STATE_KEY = "global";
 225
 226function clonePolicyConfig(input: BrowserRequestPolicyConfig): BrowserRequestPolicyConfig {
 227  return {
 228    backoff: {
 229      baseMs: input.backoff.baseMs,
 230      maxMs: input.backoff.maxMs
 231    },
 232    circuitBreaker: {
 233      failureThreshold: input.circuitBreaker.failureThreshold,
 234      openMs: input.circuitBreaker.openMs
 235    },
 236    concurrency: {
 237      maxInFlightPerClientPlatform: input.concurrency.maxInFlightPerClientPlatform
 238    },
 239    jitter: {
 240      maxMs: input.jitter.maxMs,
 241      minMs: input.jitter.minMs,
 242      muMs: input.jitter.muMs,
 243      sigmaMs: input.jitter.sigmaMs
 244    },
 245    rateLimit: {
 246      requestsPerMinutePerPlatform: input.rateLimit.requestsPerMinutePerPlatform,
 247      windowMs: input.rateLimit.windowMs
 248    },
 249    staleLease: {
 250      idleMs: input.staleLease.idleMs,
 251      sweepIntervalMs: input.staleLease.sweepIntervalMs
 252    },
 253    stream: {
 254      idleTimeoutMs: input.stream.idleTimeoutMs,
 255      maxBufferedBytes: input.stream.maxBufferedBytes,
 256      maxBufferedEvents: input.stream.maxBufferedEvents,
 257      openTimeoutMs: input.stream.openTimeoutMs
 258    }
 259  };
 260}
 261
 262function mergePolicyConfig(
 263  defaults: BrowserRequestPolicyConfig,
 264  overrides: Partial<BrowserRequestPolicyConfig>
 265): BrowserRequestPolicyConfig {
 266  return {
 267    backoff: {
 268      ...defaults.backoff,
 269      ...(overrides.backoff ?? {})
 270    },
 271    circuitBreaker: {
 272      ...defaults.circuitBreaker,
 273      ...(overrides.circuitBreaker ?? {})
 274    },
 275    concurrency: {
 276      ...defaults.concurrency,
 277      ...(overrides.concurrency ?? {})
 278    },
 279    jitter: {
 280      ...defaults.jitter,
 281      ...(overrides.jitter ?? {})
 282    },
 283    rateLimit: {
 284      ...defaults.rateLimit,
 285      ...(overrides.rateLimit ?? {})
 286    },
 287    staleLease: {
 288      ...defaults.staleLease,
 289      ...(overrides.staleLease ?? {})
 290    },
 291    stream: {
 292      ...defaults.stream,
 293      ...(overrides.stream ?? {})
 294    }
 295  };
 296}
 297
 298function normalizeOptionalString(value: string | null | undefined): string | null {
 299  if (value == null) {
 300    return null;
 301  }
 302
 303  const normalized = value.trim();
 304  return normalized === "" ? null : normalized;
 305}
 306
 307function normalizeOptionalInteger(value: unknown): number | null {
 308  return typeof value === "number" && Number.isInteger(value) ? value : null;
 309}
 310
 311function normalizeIntegerList(value: unknown): number[] {
 312  if (!Array.isArray(value)) {
 313    return [];
 314  }
 315
 316  return value.filter((entry): entry is number => typeof entry === "number" && Number.isInteger(entry));
 317}
 318
 319function readOptionalStringValue(value: unknown): string | null {
 320  return typeof value === "string" ? normalizeOptionalString(value) : null;
 321}
 322
 323function asRecord(value: unknown): Record<string, unknown> | null {
 324  if (value == null || typeof value !== "object" || Array.isArray(value)) {
 325    return null;
 326  }
 327
 328  return value as Record<string, unknown>;
 329}
 330
 331function buildTargetKey(target: BrowserRequestTarget): string {
 332  return `${target.clientId}\u0000${target.platform}`;
 333}
 334
 335function prunePlatformDispatches(
 336  state: BrowserRequestPlatformState,
 337  now: number,
 338  windowMs: number
 339): void {
 340  const cutoff = now - windowMs;
 341  state.dispatches = state.dispatches.filter((timestamp) => timestamp > cutoff);
 342}
 343
 344function clamp(value: number, min: number, max: number): number {
 345  return Math.min(max, Math.max(min, value));
 346}
 347
 348function buildTimeoutPromise(
 349  setTimeoutImpl: (handler: () => void, timeoutMs: number) => TimeoutHandle,
 350  timeoutMs: number
 351): Promise<void> {
 352  return new Promise((resolve) => {
 353    setTimeoutImpl(resolve, Math.max(0, timeoutMs));
 354  });
 355}
 356
 357function maybeUnrefTimeout(handle: TimeoutHandle): void {
 358  const maybeHandle = handle as {
 359    unref?: () => void;
 360  } | null;
 361
 362  if (typeof maybeHandle?.unref === "function") {
 363    maybeHandle.unref();
 364  }
 365}
 366
 367class BrowserRequestPolicyLeaseImpl implements BrowserRequestPolicyLease {
 368  private completed = false;
 369
 370  constructor(
 371    readonly admission: BrowserRequestAdmission,
 372    readonly target: BrowserRequestTarget,
 373    private readonly onTouch: (reason?: string) => void,
 374    private readonly onComplete: (outcome: BrowserRequestLeaseOutcome) => void
 375  ) {}
 376
 377  touch(reason?: string): void {
 378    if (this.completed) {
 379      return;
 380    }
 381
 382    this.onTouch(reason);
 383  }
 384
 385  complete(outcome: BrowserRequestLeaseOutcome): void {
 386    if (this.completed) {
 387      return;
 388    }
 389
 390    this.completed = true;
 391    this.onComplete(outcome);
 392  }
 393}
 394
 395export class BrowserRequestPolicyError extends Error {
 396  constructor(
 397    readonly code: string,
 398    message: string,
 399    readonly details: Record<string, unknown> = {}
 400  ) {
 401    super(message);
 402    this.name = "BrowserRequestPolicyError";
 403  }
 404}
 405
 406export class BrowserRequestPolicyController {
 407  private readonly clearTimeoutImpl: (handle: TimeoutHandle) => void;
 408  private readonly config: BrowserRequestPolicyConfig;
 409  private nextLeaseSequence = 1;
 410  private readonly now: () => number;
 411  private readonly platforms = new Map<string, BrowserRequestPlatformState>();
 412  private persistence: BrowserRequestPolicyPersistence | null;
 413  private readonly persistenceDebounceMs: number;
 414  private persistenceDirty = false;
 415  private persistenceFlushPromise: Promise<void> | null = null;
 416  private persistenceFlushTimer: TimeoutHandle | null = null;
 417  private persistenceInitialized = false;
 418  private persistenceInitializePromise: Promise<void> | null = null;
 419  private readonly random: () => number;
 420  private readonly setTimeoutImpl: (handler: () => void, timeoutMs: number) => TimeoutHandle;
 421  private staleLeaseSweepTimer: TimeoutHandle | null = null;
 422  private readonly targets = new Map<string, BrowserRequestTargetState>();
 423
 424  constructor(options: BrowserRequestPolicyControllerOptions = {}) {
 425    this.config = clonePolicyConfig(
 426      mergePolicyConfig(DEFAULT_BROWSER_REQUEST_POLICY, options.config ?? {})
 427    );
 428    this.clearTimeoutImpl = options.clearTimeoutImpl ?? ((handle) => globalThis.clearTimeout(handle));
 429    this.now = options.now ?? (() => Date.now());
 430    this.persistence = options.persistence ?? null;
 431    this.persistenceDebounceMs = Math.max(
 432      0,
 433      options.persistenceDebounceMs ?? DEFAULT_BROWSER_REQUEST_POLICY_PERSISTENCE_DEBOUNCE_MS
 434    );
 435    this.random = options.random ?? (() => Math.random());
 436    this.setTimeoutImpl =
 437      options.setTimeoutImpl ?? ((handler, timeoutMs) => globalThis.setTimeout(handler, timeoutMs));
 438    this.scheduleNextStaleLeaseSweep();
 439  }
 440
 441  async initialize(): Promise<void> {
 442    await this.ensurePersistenceInitialized();
 443  }
 444
 445  async flush(): Promise<void> {
 446    await this.ensurePersistenceInitialized();
 447
 448    if (this.persistence == null) {
 449      return;
 450    }
 451
 452    if (this.persistenceFlushTimer != null) {
 453      this.clearTimeoutImpl(this.persistenceFlushTimer);
 454      this.persistenceFlushTimer = null;
 455    }
 456
 457    if (this.persistenceFlushPromise != null) {
 458      await this.persistenceFlushPromise;
 459    }
 460
 461    await this.flushPersistentState();
 462  }
 463
 464  getConfig(): BrowserRequestPolicyConfig {
 465    return clonePolicyConfig(this.config);
 466  }
 467
 468  getSnapshot(): BrowserRequestPolicySnapshot {
 469    const now = this.now();
 470    const platforms = [...this.platforms.entries()]
 471      .map(([platform, state]) => {
 472        prunePlatformDispatches(state, now, this.config.rateLimit.windowMs);
 473        return {
 474          lastDispatchedAt: state.lastDispatchedAt,
 475          platform,
 476          recentDispatchCount: state.dispatches.length,
 477          waiting: state.waiters.length
 478        };
 479      })
 480      .sort((left, right) => left.platform.localeCompare(right.platform));
 481    const targets = [...this.targets.entries()]
 482      .map(([key, state]) => {
 483        const [clientId, platform] = key.split("\u0000");
 484        return {
 485          backoffUntil: state.backoffUntil,
 486          circuitRetryAt: state.circuitRetryAt,
 487          circuitState: state.circuitState,
 488          clientId: clientId ?? "",
 489          consecutiveFailures: state.consecutiveFailures,
 490          inFlight: state.inFlight,
 491          lastActivityAt: state.lastActivityAt,
 492          lastActivityReason: state.lastActivityReason,
 493          lastError: state.lastError,
 494          lastFailureAt: state.lastFailureAt,
 495          lastStaleSweepAt: state.lastStaleSweepAt,
 496          lastStaleSweepIdleMs: state.lastStaleSweepIdleMs,
 497          lastStaleSweepReason: state.lastStaleSweepReason,
 498          lastStaleSweepRequestId: state.lastStaleSweepRequestId,
 499          lastSuccessAt: state.lastSuccessAt,
 500          platform: platform ?? "",
 501          staleSweepCount: state.staleSweepCount,
 502          waiting: state.waiters.length
 503        };
 504      })
 505      .sort((left, right) => {
 506        const clientCompare = left.clientId.localeCompare(right.clientId);
 507        return clientCompare === 0 ? left.platform.localeCompare(right.platform) : clientCompare;
 508      });
 509
 510    return {
 511      defaults: this.getConfig(),
 512      platforms,
 513      targets
 514    };
 515  }
 516
 517  async beginRequest(
 518    target: BrowserRequestTarget,
 519    requestId: string
 520  ): Promise<BrowserRequestPolicyLease> {
 521    await this.ensurePersistenceInitialized();
 522
 523    const normalizedTarget = this.normalizeTarget(target);
 524    const requestedAt = this.now();
 525    const targetState = this.getTargetState(normalizedTarget);
 526    this.sweepStaleTargetLeases(targetState, requestedAt, "request_begin");
 527    const leaseId = await this.acquireTargetSlot(normalizedTarget, targetState, requestId);
 528    let admission: BrowserRequestAdmission | null = null;
 529
 530    try {
 531      admission = await this.admitRequest(normalizedTarget, targetState, leaseId, requestId, requestedAt);
 532    } catch (error) {
 533      this.releaseTargetLease(targetState, leaseId);
 534      throw error;
 535    }
 536
 537    return new BrowserRequestPolicyLeaseImpl(
 538      admission,
 539      normalizedTarget,
 540      (reason) => {
 541        this.touchLease(targetState, leaseId, reason);
 542      },
 543      (outcome) => {
 544        this.completeRequest(targetState, leaseId, outcome);
 545      }
 546    );
 547  }
 548
 549  private async ensurePersistenceInitialized(): Promise<void> {
 550    if (this.persistence == null || this.persistenceInitialized) {
 551      return;
 552    }
 553
 554    if (this.persistenceInitializePromise != null) {
 555      await this.persistenceInitializePromise;
 556      return;
 557    }
 558
 559    this.persistenceInitializePromise = (async () => {
 560      try {
 561        const loadedState = await this.persistence?.load();
 562
 563        if (loadedState != null) {
 564          this.restorePersistentState(loadedState);
 565        }
 566
 567        this.persistenceInitialized = true;
 568      } finally {
 569        this.persistenceInitializePromise = null;
 570      }
 571    })();
 572
 573    await this.persistenceInitializePromise;
 574  }
 575
 576  private restorePersistentState(state: BrowserRequestPolicyPersistentState): void {
 577    const now = this.now();
 578    const platforms = Array.isArray(state.platforms) ? state.platforms : [];
 579    const targets = Array.isArray(state.targets) ? state.targets : [];
 580
 581    for (const entry of platforms) {
 582      const platform = normalizeOptionalString(entry.platform);
 583
 584      if (platform == null) {
 585        continue;
 586      }
 587
 588      const restored: BrowserRequestPlatformState = {
 589        busy: false,
 590        dispatches: normalizeIntegerList(entry.dispatches).sort((left, right) => left - right),
 591        lastDispatchedAt: normalizeOptionalInteger(entry.lastDispatchedAt),
 592        waiters: []
 593      };
 594      prunePlatformDispatches(restored, now, this.config.rateLimit.windowMs);
 595      this.platforms.set(platform, restored);
 596    }
 597
 598    for (const entry of targets) {
 599      const clientId = normalizeOptionalString(entry.clientId);
 600      const platform = normalizeOptionalString(entry.platform);
 601
 602      if (clientId == null || platform == null) {
 603        continue;
 604      }
 605
 606      const key = buildTargetKey({
 607        clientId,
 608        platform
 609      });
 610      this.targets.set(key, {
 611        backoffUntil: normalizeOptionalInteger(entry.backoffUntil),
 612        circuitRetryAt: normalizeOptionalInteger(entry.circuitRetryAt),
 613        circuitState: entry.circuitState === "half_open" || entry.circuitState === "open"
 614          ? entry.circuitState
 615          : "closed",
 616        consecutiveFailures: Math.max(0, normalizeOptionalInteger(entry.consecutiveFailures) ?? 0),
 617        inFlight: 0,
 618        lastActivityAt: normalizeOptionalInteger(entry.lastActivityAt),
 619        lastActivityReason: normalizeOptionalString(entry.lastActivityReason),
 620        lastError: normalizeOptionalString(entry.lastError),
 621        lastFailureAt: normalizeOptionalInteger(entry.lastFailureAt),
 622        lastStaleSweepAt: normalizeOptionalInteger(entry.lastStaleSweepAt),
 623        lastStaleSweepIdleMs: normalizeOptionalInteger(entry.lastStaleSweepIdleMs),
 624        lastStaleSweepReason: normalizeOptionalString(entry.lastStaleSweepReason),
 625        lastStaleSweepRequestId: normalizeOptionalString(entry.lastStaleSweepRequestId),
 626        lastSuccessAt: normalizeOptionalInteger(entry.lastSuccessAt),
 627        leases: new Map(),
 628        staleSweepCount: Math.max(0, normalizeOptionalInteger(entry.staleSweepCount) ?? 0),
 629        waiters: []
 630      });
 631    }
 632  }
 633
 634  private markPersistentStateDirty(): void {
 635    if (this.persistence == null) {
 636      return;
 637    }
 638
 639    this.persistenceDirty = true;
 640    this.schedulePersistentFlush();
 641  }
 642
 643  private schedulePersistentFlush(): void {
 644    if (
 645      this.persistence == null
 646      || !this.persistenceInitialized
 647      || this.persistenceFlushPromise != null
 648      || this.persistenceFlushTimer != null
 649    ) {
 650      return;
 651    }
 652
 653    this.persistenceFlushTimer = this.setTimeoutImpl(() => {
 654      this.persistenceFlushTimer = null;
 655      void this.flushPersistentState().catch(() => {
 656        this.persistenceDirty = true;
 657        this.schedulePersistentFlush();
 658      });
 659    }, this.persistenceDebounceMs);
 660    maybeUnrefTimeout(this.persistenceFlushTimer);
 661  }
 662
 663  private async flushPersistentState(): Promise<void> {
 664    if (
 665      this.persistence == null
 666      || !this.persistenceInitialized
 667      || this.persistenceFlushPromise != null
 668      || !this.persistenceDirty
 669    ) {
 670      return;
 671    }
 672
 673    this.persistenceFlushPromise = (async () => {
 674      try {
 675        while (this.persistenceDirty) {
 676          this.persistenceDirty = false;
 677          await this.persistence?.save(this.buildPersistentState());
 678        }
 679      } finally {
 680        this.persistenceFlushPromise = null;
 681
 682        if (this.persistenceDirty) {
 683          this.schedulePersistentFlush();
 684        }
 685      }
 686    })();
 687
 688    await this.persistenceFlushPromise;
 689  }
 690
 691  private buildPersistentState(): BrowserRequestPolicyPersistentState {
 692    const now = this.now();
 693    const platforms = [...this.platforms.entries()]
 694      .map(([platform, state]) => {
 695        prunePlatformDispatches(state, now, this.config.rateLimit.windowMs);
 696        return {
 697          dispatches: [...state.dispatches],
 698          lastDispatchedAt: state.lastDispatchedAt,
 699          platform
 700        };
 701      })
 702      .sort((left, right) => left.platform.localeCompare(right.platform));
 703    const targets = [...this.targets.entries()]
 704      .map(([key, state]) => {
 705        const [clientId, platform] = key.split("\u0000");
 706        return {
 707          backoffUntil: state.backoffUntil,
 708          circuitRetryAt: state.circuitRetryAt,
 709          circuitState: state.circuitState,
 710          clientId: clientId ?? "",
 711          consecutiveFailures: state.consecutiveFailures,
 712          lastActivityAt: state.lastActivityAt,
 713          lastActivityReason: state.lastActivityReason,
 714          lastError: state.lastError,
 715          lastFailureAt: state.lastFailureAt,
 716          lastStaleSweepAt: state.lastStaleSweepAt,
 717          lastStaleSweepIdleMs: state.lastStaleSweepIdleMs,
 718          lastStaleSweepReason: state.lastStaleSweepReason,
 719          lastStaleSweepRequestId: state.lastStaleSweepRequestId,
 720          lastSuccessAt: state.lastSuccessAt,
 721          platform: platform ?? "",
 722          staleSweepCount: state.staleSweepCount
 723        };
 724      })
 725      .sort((left, right) => {
 726        const clientCompare = left.clientId.localeCompare(right.clientId);
 727        return clientCompare === 0 ? left.platform.localeCompare(right.platform) : clientCompare;
 728      });
 729
 730    return {
 731      platforms,
 732      targets,
 733      version: 1
 734    };
 735  }
 736
 737  private normalizeTarget(target: BrowserRequestTarget): BrowserRequestTarget {
 738    const clientId = normalizeOptionalString(target.clientId);
 739    const platform = normalizeOptionalString(target.platform);
 740
 741    if (clientId == null || platform == null) {
 742      throw new Error("Browser request policy requires non-empty clientId and platform.");
 743    }
 744
 745    return {
 746      clientId,
 747      platform
 748    };
 749  }
 750
 751  private getPlatformState(platform: string): BrowserRequestPlatformState {
 752    const existing = this.platforms.get(platform);
 753
 754    if (existing != null) {
 755      return existing;
 756    }
 757
 758    const created: BrowserRequestPlatformState = {
 759      busy: false,
 760      dispatches: [],
 761      lastDispatchedAt: null,
 762      waiters: []
 763    };
 764    this.platforms.set(platform, created);
 765    return created;
 766  }
 767
 768  private getTargetState(target: BrowserRequestTarget): BrowserRequestTargetState {
 769    const key = buildTargetKey(target);
 770    const existing = this.targets.get(key);
 771
 772    if (existing != null) {
 773      return existing;
 774    }
 775
 776    const created: BrowserRequestTargetState = {
 777      backoffUntil: null,
 778      circuitRetryAt: null,
 779      circuitState: "closed",
 780      consecutiveFailures: 0,
 781      inFlight: 0,
 782      lastActivityAt: null,
 783      lastActivityReason: null,
 784      lastError: null,
 785      lastFailureAt: null,
 786      lastStaleSweepAt: null,
 787      lastStaleSweepIdleMs: null,
 788      lastStaleSweepReason: null,
 789      lastStaleSweepRequestId: null,
 790      lastSuccessAt: null,
 791      leases: new Map(),
 792      staleSweepCount: 0,
 793      waiters: []
 794    };
 795    this.targets.set(key, created);
 796    return created;
 797  }
 798
 799  private async waitForWaiter(
 800    waiters: Array<() => void>,
 801    buildError: () => BrowserRequestPolicyError
 802  ): Promise<void> {
 803    await new Promise<void>((resolve, reject) => {
 804      let settled = false;
 805      let timer: TimeoutHandle | null = null;
 806      const onReady = () => {
 807        if (settled) {
 808          return;
 809        }
 810
 811        settled = true;
 812        if (timer != null) {
 813          this.clearTimeoutImpl(timer);
 814        }
 815        resolve();
 816      };
 817
 818      timer = this.setTimeoutImpl(() => {
 819        if (settled) {
 820          return;
 821        }
 822
 823        settled = true;
 824        const index = waiters.indexOf(onReady);
 825        if (index !== -1) {
 826          waiters.splice(index, 1);
 827        }
 828        reject(buildError());
 829      }, BROWSER_REQUEST_WAITER_TIMEOUT_MS);
 830
 831      waiters.push(onReady);
 832    });
 833  }
 834
 835  private async acquireTargetSlot(
 836    target: BrowserRequestTarget,
 837    state: BrowserRequestTargetState,
 838    requestId: string
 839  ): Promise<string> {
 840    if (
 841      state.inFlight < this.config.concurrency.maxInFlightPerClientPlatform
 842      && state.waiters.length === 0
 843    ) {
 844      state.inFlight += 1;
 845      return this.createLeaseRuntime(state, requestId, "slot_acquired");
 846    }
 847
 848    await this.waitForWaiter(
 849      state.waiters,
 850      () =>
 851        new BrowserRequestPolicyError(
 852          "waiter_timeout",
 853          `Timed out waiting for a browser request slot for ${target.platform} on client "${target.clientId}".`,
 854          {
 855            client_id: target.clientId,
 856            platform: target.platform,
 857            timeout_ms: BROWSER_REQUEST_WAITER_TIMEOUT_MS,
 858            wait_scope: "target_slot"
 859          }
 860        )
 861    );
 862
 863    return this.createLeaseRuntime(state, requestId, "slot_acquired");
 864  }
 865
 866  private releaseTargetSlot(state: BrowserRequestTargetState): void {
 867    const waiter = state.waiters.shift();
 868
 869    if (waiter != null) {
 870      waiter();
 871      return;
 872    }
 873
 874    state.inFlight = Math.max(0, state.inFlight - 1);
 875  }
 876
 877  private createLeaseRuntime(
 878    state: BrowserRequestTargetState,
 879    requestId: string,
 880    reason: string
 881  ): string {
 882    const now = this.now();
 883    const leaseId = `${requestId}\u0000${this.nextLeaseSequence++}`;
 884    state.leases.set(leaseId, {
 885      admittedAt: null,
 886      createdAt: now,
 887      lastActivityAt: now,
 888      lastActivityReason: reason,
 889      requestId
 890    });
 891    this.recordTargetActivity(state, now, reason);
 892    return leaseId;
 893  }
 894
 895  private recordTargetActivity(
 896    state: BrowserRequestTargetState,
 897    at: number,
 898    reason: string
 899  ): void {
 900    state.lastActivityAt = at;
 901    state.lastActivityReason = normalizeOptionalString(reason);
 902  }
 903
 904  private touchLease(
 905    state: BrowserRequestTargetState,
 906    leaseId: string,
 907    reason?: string
 908  ): void {
 909    const lease = state.leases.get(leaseId);
 910    if (lease == null) {
 911      return;
 912    }
 913
 914    const now = this.now();
 915    lease.lastActivityAt = now;
 916    lease.lastActivityReason = normalizeOptionalString(reason) ?? "lease_touch";
 917    this.recordTargetActivity(state, now, lease.lastActivityReason);
 918  }
 919
 920  private markLeaseAdmitted(
 921    state: BrowserRequestTargetState,
 922    leaseId: string,
 923    admittedAt: number
 924  ): void {
 925    const lease = state.leases.get(leaseId);
 926    if (lease == null) {
 927      return;
 928    }
 929
 930    lease.admittedAt = admittedAt;
 931    lease.lastActivityAt = admittedAt;
 932    lease.lastActivityReason = "admitted";
 933    this.recordTargetActivity(state, admittedAt, "admitted");
 934  }
 935
 936  private releaseTargetLease(
 937    state: BrowserRequestTargetState,
 938    leaseId: string
 939  ): void {
 940    if (!state.leases.delete(leaseId)) {
 941      return;
 942    }
 943
 944    this.releaseTargetSlot(state);
 945  }
 946
 947  private async acquirePlatformAdmission(
 948    target: BrowserRequestTarget,
 949    state: BrowserRequestPlatformState
 950  ): Promise<void> {
 951    if (!state.busy) {
 952      state.busy = true;
 953      return;
 954    }
 955
 956    await this.waitForWaiter(
 957      state.waiters,
 958      () =>
 959        new BrowserRequestPolicyError(
 960          "waiter_timeout",
 961          `Timed out waiting for browser request admission on platform "${target.platform}".`,
 962          {
 963            client_id: target.clientId,
 964            platform: target.platform,
 965            timeout_ms: BROWSER_REQUEST_WAITER_TIMEOUT_MS,
 966            wait_scope: "platform_admission"
 967          }
 968        )
 969    );
 970  }
 971
 972  private releasePlatformAdmission(state: BrowserRequestPlatformState): void {
 973    const waiter = state.waiters.shift();
 974
 975    if (waiter != null) {
 976      waiter();
 977      return;
 978    }
 979
 980    state.busy = false;
 981  }
 982
 983  private async admitRequest(
 984    target: BrowserRequestTarget,
 985    state: BrowserRequestTargetState,
 986    leaseId: string,
 987    requestId: string,
 988    requestedAt: number
 989  ): Promise<BrowserRequestAdmission> {
 990    const platformState = this.getPlatformState(target.platform);
 991    await this.acquirePlatformAdmission(target, platformState);
 992    let backoffDelayMs = 0;
 993    let jitterDelayMs = 0;
 994    let rateLimitDelayMs = 0;
 995
 996    try {
 997      this.assertCircuitAllowsRequest(target, state);
 998
 999      const backoffUntil = state.backoffUntil ?? 0;
1000      const now = this.now();
1001      if (backoffUntil > now) {
1002        backoffDelayMs = backoffUntil - now;
1003        this.touchLease(state, leaseId, "backoff_wait");
1004        await buildTimeoutPromise(this.setTimeoutImpl, backoffDelayMs);
1005        this.touchLease(state, leaseId, "backoff_complete");
1006      }
1007
1008      const nowAfterBackoff = this.now();
1009      prunePlatformDispatches(platformState, nowAfterBackoff, this.config.rateLimit.windowMs);
1010
1011      if (
1012        platformState.dispatches.length >= this.config.rateLimit.requestsPerMinutePerPlatform
1013      ) {
1014        const earliest = platformState.dispatches[0] ?? nowAfterBackoff;
1015        rateLimitDelayMs = Math.max(
1016          0,
1017          earliest + this.config.rateLimit.windowMs - nowAfterBackoff
1018        );
1019
1020        if (rateLimitDelayMs > 0) {
1021          this.touchLease(state, leaseId, "rate_limit_wait");
1022          await buildTimeoutPromise(this.setTimeoutImpl, rateLimitDelayMs);
1023          this.touchLease(state, leaseId, "rate_limit_complete");
1024        }
1025      }
1026
1027      jitterDelayMs = this.sampleJitterDelayMs();
1028      if (jitterDelayMs > 0) {
1029        this.touchLease(state, leaseId, "jitter_wait");
1030        await buildTimeoutPromise(this.setTimeoutImpl, jitterDelayMs);
1031        this.touchLease(state, leaseId, "jitter_complete");
1032      }
1033
1034      const admittedAt = this.now();
1035      this.markLeaseAdmitted(state, leaseId, admittedAt);
1036      prunePlatformDispatches(platformState, admittedAt, this.config.rateLimit.windowMs);
1037      platformState.dispatches.push(admittedAt);
1038      platformState.lastDispatchedAt = admittedAt;
1039      this.markPersistentStateDirty();
1040
1041      return {
1042        admittedAt,
1043        backoffDelayMs,
1044        circuitState: state.circuitState,
1045        jitterDelayMs,
1046        platform: target.platform,
1047        queueDelayMs: Math.max(
1048          0,
1049          admittedAt - requestedAt - backoffDelayMs - rateLimitDelayMs - jitterDelayMs
1050        ),
1051        rateLimitDelayMs,
1052        requestId,
1053        requestedAt,
1054        targetClientId: target.clientId
1055      };
1056    } finally {
1057      this.releasePlatformAdmission(platformState);
1058    }
1059  }
1060
1061  private assertCircuitAllowsRequest(
1062    target: BrowserRequestTarget,
1063    state: BrowserRequestTargetState
1064  ): void {
1065    if (state.circuitState !== "open") {
1066      return;
1067    }
1068
1069    const now = this.now();
1070    const retryAt = state.circuitRetryAt ?? 0;
1071
1072    if (retryAt > now) {
1073      throw new BrowserRequestPolicyError(
1074        "circuit_open",
1075        `Browser request circuit is open for ${target.platform} on client "${target.clientId}".`,
1076        {
1077          client_id: target.clientId,
1078          platform: target.platform,
1079          retry_after_ms: retryAt - now
1080        }
1081      );
1082    }
1083
1084    state.circuitState = "half_open";
1085    this.markPersistentStateDirty();
1086  }
1087
1088  private sampleJitterDelayMs(): number {
1089    const { maxMs, minMs, muMs, sigmaMs } = this.config.jitter;
1090    const u1 = Math.max(Number.EPSILON, this.random());
1091    const u2 = Math.max(Number.EPSILON, this.random());
1092    const gaussian = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
1093    return Math.round(clamp(muMs + gaussian * sigmaMs, minMs, maxMs));
1094  }
1095
1096  private completeRequest(
1097    state: BrowserRequestTargetState,
1098    leaseId: string,
1099    outcome: BrowserRequestLeaseOutcome
1100  ): void {
1101    if (!state.leases.has(leaseId)) {
1102      return;
1103    }
1104
1105    const now = this.now();
1106
1107    if (outcome.status === "success") {
1108      state.backoffUntil = null;
1109      state.circuitRetryAt = null;
1110      state.circuitState = "closed";
1111      state.consecutiveFailures = 0;
1112      state.lastError = null;
1113      state.lastSuccessAt = now;
1114      this.recordTargetActivity(state, now, "complete_success");
1115      this.markPersistentStateDirty();
1116      this.releaseTargetLease(state, leaseId);
1117      return;
1118    }
1119
1120    if (outcome.status === "cancelled") {
1121      this.recordTargetActivity(state, now, "complete_cancelled");
1122      this.releaseTargetLease(state, leaseId);
1123      return;
1124    }
1125
1126    state.consecutiveFailures += 1;
1127    state.lastError = normalizeOptionalString(outcome.code) ?? normalizeOptionalString(outcome.message);
1128    state.lastFailureAt = now;
1129    const backoffDelay = Math.min(
1130      this.config.backoff.maxMs,
1131      this.config.backoff.baseMs * Math.pow(2, Math.max(0, state.consecutiveFailures - 1))
1132    );
1133    state.backoffUntil = now + backoffDelay;
1134
1135    if (
1136      state.circuitState === "half_open"
1137      || state.consecutiveFailures >= this.config.circuitBreaker.failureThreshold
1138    ) {
1139      state.circuitState = "open";
1140      state.circuitRetryAt = now + this.config.circuitBreaker.openMs;
1141    }
1142
1143    this.recordTargetActivity(state, now, "complete_failure");
1144    this.markPersistentStateDirty();
1145    this.releaseTargetLease(state, leaseId);
1146  }
1147
1148  private scheduleNextStaleLeaseSweep(): void {
1149    if (
1150      this.config.staleLease.idleMs <= 0
1151      || this.config.staleLease.sweepIntervalMs <= 0
1152      || this.staleLeaseSweepTimer != null
1153    ) {
1154      return;
1155    }
1156
1157    this.staleLeaseSweepTimer = this.setTimeoutImpl(() => {
1158      this.staleLeaseSweepTimer = null;
1159
1160      try {
1161        this.sweepStaleLeases();
1162      } finally {
1163        this.scheduleNextStaleLeaseSweep();
1164      }
1165    }, this.config.staleLease.sweepIntervalMs);
1166    maybeUnrefTimeout(this.staleLeaseSweepTimer);
1167  }
1168
1169  private sweepStaleLeases(): void {
1170    const now = this.now();
1171
1172    for (const state of this.targets.values()) {
1173      this.sweepStaleTargetLeases(state, now, "background_interval");
1174    }
1175  }
1176
1177  private sweepStaleTargetLeases(
1178    state: BrowserRequestTargetState,
1179    now: number,
1180    reason: string
1181  ): void {
1182    if (state.inFlight === 0 || state.leases.size === 0) {
1183      return;
1184    }
1185
1186    const staleLeaseIds = [...state.leases.entries()]
1187      .filter(([, lease]) => now - lease.lastActivityAt >= this.config.staleLease.idleMs)
1188      .map(([leaseId]) => leaseId);
1189
1190    for (const leaseId of staleLeaseIds) {
1191      this.sweepStaleTargetLease(state, leaseId, now, reason);
1192    }
1193  }
1194
1195  private sweepStaleTargetLease(
1196    state: BrowserRequestTargetState,
1197    leaseId: string,
1198    now: number,
1199    reason: string
1200  ): void {
1201    const lease = state.leases.get(leaseId);
1202    if (lease == null) {
1203      return;
1204    }
1205
1206    state.staleSweepCount += 1;
1207    state.lastStaleSweepAt = now;
1208    state.lastStaleSweepIdleMs = Math.max(0, now - lease.lastActivityAt);
1209    state.lastStaleSweepReason = `${reason}:lease_idle_timeout`;
1210    state.lastStaleSweepRequestId = lease.requestId;
1211    this.recordTargetActivity(state, now, "stale_sweep");
1212    this.markPersistentStateDirty();
1213    this.releaseTargetLease(state, leaseId);
1214  }
1215}
1216
1217function parsePersistentStateJson(valueJson: string): BrowserRequestPolicyPersistentState | null {
1218  try {
1219    const parsed = asRecord(JSON.parse(valueJson) as unknown);
1220
1221    if (parsed == null) {
1222      return null;
1223    }
1224
1225    const version = parsed.version === 1 ? 1 : null;
1226
1227    if (version == null) {
1228      return null;
1229    }
1230
1231    const platforms = Array.isArray(parsed.platforms)
1232      ? parsed.platforms
1233        .map((entry) => {
1234          const record = asRecord(entry);
1235          const platform = readOptionalStringValue(record?.platform);
1236
1237          if (record == null || platform == null) {
1238            return null;
1239          }
1240
1241          return {
1242            dispatches: normalizeIntegerList(record.dispatches),
1243            lastDispatchedAt: normalizeOptionalInteger(record.lastDispatchedAt),
1244            platform
1245          };
1246        })
1247        .filter((entry): entry is BrowserRequestPolicyPersistentPlatformState => entry != null)
1248      : [];
1249    const targets = Array.isArray(parsed.targets)
1250      ? parsed.targets
1251        .map((entry) => {
1252          const record = asRecord(entry);
1253          const clientId = readOptionalStringValue(record?.clientId);
1254          const platform = readOptionalStringValue(record?.platform);
1255
1256          if (record == null || clientId == null || platform == null) {
1257            return null;
1258          }
1259
1260          return {
1261            backoffUntil: normalizeOptionalInteger(record.backoffUntil),
1262            circuitRetryAt: normalizeOptionalInteger(record.circuitRetryAt),
1263            circuitState: record.circuitState === "half_open" || record.circuitState === "open"
1264              ? record.circuitState
1265              : "closed",
1266            clientId,
1267            consecutiveFailures: Math.max(0, normalizeOptionalInteger(record.consecutiveFailures) ?? 0),
1268            lastActivityAt: normalizeOptionalInteger(record.lastActivityAt),
1269            lastActivityReason: readOptionalStringValue(record.lastActivityReason),
1270            lastError: readOptionalStringValue(record.lastError),
1271            lastFailureAt: normalizeOptionalInteger(record.lastFailureAt),
1272            lastStaleSweepAt: normalizeOptionalInteger(record.lastStaleSweepAt),
1273            lastStaleSweepIdleMs: normalizeOptionalInteger(record.lastStaleSweepIdleMs),
1274            lastStaleSweepReason: readOptionalStringValue(record.lastStaleSweepReason),
1275            lastStaleSweepRequestId: readOptionalStringValue(record.lastStaleSweepRequestId),
1276            lastSuccessAt: normalizeOptionalInteger(record.lastSuccessAt),
1277            platform,
1278            staleSweepCount: Math.max(0, normalizeOptionalInteger(record.staleSweepCount) ?? 0)
1279          };
1280        })
1281        .filter((entry): entry is BrowserRequestPolicyPersistentTargetState => entry != null)
1282      : [];
1283
1284    return {
1285      platforms,
1286      targets,
1287      version
1288    };
1289  } catch {
1290    return null;
1291  }
1292}
1293
1294export function createArtifactStoreBrowserRequestPolicyPersistence(
1295  artifactStore: Pick<ArtifactStore, "getBrowserRequestPolicyState" | "upsertBrowserRequestPolicyState">,
1296  options: {
1297    now?: () => number;
1298    stateKey?: string;
1299  } = {}
1300): BrowserRequestPolicyPersistence {
1301  const now = options.now ?? (() => Date.now());
1302  const stateKey = normalizeOptionalString(options.stateKey) ?? DEFAULT_BROWSER_REQUEST_POLICY_STATE_KEY;
1303
1304  return {
1305    async load() {
1306      const record = await artifactStore.getBrowserRequestPolicyState(stateKey);
1307      return record == null ? null : parsePersistentStateJson(record.valueJson);
1308    },
1309    async save(state) {
1310      await artifactStore.upsertBrowserRequestPolicyState({
1311        stateKey,
1312        updatedAt: now(),
1313        valueJson: JSON.stringify(state)
1314      });
1315    }
1316  };
1317}
1318
1319export function createDefaultBrowserRequestPolicyConfig(): BrowserRequestPolicyConfig {
1320  return clonePolicyConfig(DEFAULT_BROWSER_REQUEST_POLICY);
1321}