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}