@nijaru/pi-search 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/brave.ts +38 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nijaru/pi-search",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Provider-neutral web search and fetching tools for Pi",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/brave.ts CHANGED
@@ -25,7 +25,7 @@ export type BraveFetch = (input: string | URL | Request, init?: RequestInit) =>
25
25
  export interface BraveCapacityTracker {
26
26
  /** True when no known finite quota window currently blocks a call. */
27
27
  readonly canAttempt: () => boolean;
28
- /** Reserve a local request slot, returning false when the caller canceled. */
28
+ /** Wait for local admission, returning false when the caller canceled. */
29
29
  readonly waitForTurn?: (signal: AbortSignal) => Promise<boolean>;
30
30
  /** Record provider observations without inventing or persisting counters. */
31
31
  readonly observe: (info: ProviderRateLimitInfo | undefined) => void;
@@ -48,6 +48,8 @@ export class BraveQuotaTracker implements BraveCapacityTracker {
48
48
  private observedAtMs = 0;
49
49
  private readonly minimumIntervalMs: number;
50
50
  private nextRequestAtMs = 0;
51
+ private readonly waiting = new Set<{ signal: AbortSignal; finish: (admitted: boolean) => void }>();
52
+ private admissionTimer: ReturnType<typeof setTimeout> | undefined;
51
53
 
52
54
  constructor(options: BraveQuotaTrackerOptions = {}) {
53
55
  const minimumIntervalMs = options.minimumIntervalMs ?? 0;
@@ -57,30 +59,48 @@ export class BraveQuotaTracker implements BraveCapacityTracker {
57
59
  this.minimumIntervalMs = minimumIntervalMs;
58
60
  }
59
61
 
60
- async waitForTurn(signal: AbortSignal): Promise<boolean> {
61
- if (signal.aborted) return false;
62
- const now = Date.now();
63
- const startAt = Math.max(now, this.nextRequestAtMs);
64
- this.nextRequestAtMs = startAt + this.minimumIntervalMs;
65
- const delayMs = startAt - now;
66
- if (delayMs <= 0) return true;
62
+ waitForTurn(signal: AbortSignal): Promise<boolean> {
63
+ if (signal.aborted) return Promise.resolve(false);
64
+ if (this.minimumIntervalMs === 0) return Promise.resolve(true);
67
65
  return new Promise<boolean>((resolve) => {
68
- let settled = false;
69
- let timer: ReturnType<typeof setTimeout> | undefined;
70
- const finish = (admitted: boolean): void => {
71
- if (settled) return;
72
- settled = true;
73
- if (timer !== undefined) clearTimeout(timer);
74
- signal.removeEventListener("abort", onAbort);
75
- resolve(admitted);
66
+ const waiter = {
67
+ signal,
68
+ finish: (admitted: boolean): void => {
69
+ if (!this.waiting.delete(waiter)) return;
70
+ signal.removeEventListener("abort", onAbort);
71
+ if (this.waiting.size === 0 && this.admissionTimer !== undefined) {
72
+ clearTimeout(this.admissionTimer);
73
+ this.admissionTimer = undefined;
74
+ }
75
+ resolve(admitted);
76
+ },
76
77
  };
77
- const onAbort = (): void => finish(false);
78
- timer = setTimeout(() => finish(true), delayMs);
78
+ const onAbort = (): void => { waiter.finish(false); };
79
+ this.waiting.add(waiter);
79
80
  signal.addEventListener("abort", onAbort, { once: true });
80
81
  if (signal.aborted) onAbort();
82
+ this.admitNext();
81
83
  });
82
84
  }
83
85
 
86
+ private admitNext(): void {
87
+ if (this.admissionTimer !== undefined) return;
88
+ for (const waiter of this.waiting) {
89
+ if (waiter.signal.aborted) { waiter.finish(false); continue; }
90
+ const now = Date.now(), delayMs = this.nextRequestAtMs - now;
91
+ if (delayMs > 0) {
92
+ this.admissionTimer = setTimeout(() => {
93
+ this.admissionTimer = undefined;
94
+ this.admitNext();
95
+ }, delayMs);
96
+ return;
97
+ }
98
+ // Only actual admissions consume spacing. Late timers cannot release a burst.
99
+ this.nextRequestAtMs = now + this.minimumIntervalMs;
100
+ waiter.finish(true);
101
+ }
102
+ }
103
+
84
104
  canAttempt(): boolean {
85
105
  const info = this.snapshot();
86
106
  if (info === undefined) return true;