@bbradar/mcp 0.2.5 → 0.2.6
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.
- package/CHANGELOG.md +13 -0
- package/README.md +47 -9
- package/benchmark/compare-revisions.mjs +164 -0
- package/benchmark/pro-benchmark.mjs +84 -0
- package/dist/bbradarClient.d.ts +16 -1
- package/dist/bbradarClient.js +213 -40
- package/dist/bbradarClient.js.map +1 -1
- package/dist/sanitize.d.ts +2 -0
- package/dist/sanitize.js +47 -13
- package/dist/sanitize.js.map +1 -1
- package/dist/server.js +237 -46
- package/dist/server.js.map +1 -1
- package/package.json +5 -3
package/dist/bbradarClient.js
CHANGED
|
@@ -1,7 +1,75 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
3
|
import { TtlCache } from "./cache.js";
|
|
3
4
|
import { stableStringify } from "./json.js";
|
|
4
5
|
import { encodeProgramIdPath } from "./url.js";
|
|
6
|
+
function publicPayload(payload) {
|
|
7
|
+
const { cacheAllowed: _cacheAllowed, ...publicFields } = payload;
|
|
8
|
+
return publicFields;
|
|
9
|
+
}
|
|
10
|
+
function subscribeToRequest(promise, requestId, signal, shared) {
|
|
11
|
+
if (shared)
|
|
12
|
+
shared.consumers += 1;
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
let settled = false;
|
|
15
|
+
const finish = (callback) => {
|
|
16
|
+
if (settled)
|
|
17
|
+
return;
|
|
18
|
+
settled = true;
|
|
19
|
+
signal?.removeEventListener("abort", cancel);
|
|
20
|
+
if (shared)
|
|
21
|
+
shared.consumers -= 1;
|
|
22
|
+
callback();
|
|
23
|
+
};
|
|
24
|
+
const cancel = () => finish(() => {
|
|
25
|
+
if (shared?.consumers === 0)
|
|
26
|
+
shared.controller.abort(signal?.reason);
|
|
27
|
+
reject(new BBRadarApiError({ message: "BBRadar API request was cancelled or exceeded its tool deadline.", requestId, status: 408 }));
|
|
28
|
+
});
|
|
29
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
30
|
+
promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
|
|
31
|
+
if (signal?.aborted)
|
|
32
|
+
cancel();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function waitForRequestSlot(queue, deadline, requestId, signal) {
|
|
36
|
+
if (queue.length >= MAX_QUEUED_REQUESTS) {
|
|
37
|
+
throw new BBRadarApiError({ message: "BBRadar API request queue is full.", requestId, status: 503 });
|
|
38
|
+
}
|
|
39
|
+
if (deadline <= Date.now() || signal?.aborted) {
|
|
40
|
+
throw new BBRadarApiError({ message: "BBRadar API request timed out or was cancelled while queued.", requestId, status: 408 });
|
|
41
|
+
}
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
let settled = false;
|
|
44
|
+
const remove = () => {
|
|
45
|
+
const index = queue.indexOf(grant);
|
|
46
|
+
if (index >= 0)
|
|
47
|
+
queue.splice(index, 1);
|
|
48
|
+
};
|
|
49
|
+
const cleanup = () => {
|
|
50
|
+
clearTimeout(timer);
|
|
51
|
+
signal?.removeEventListener("abort", cancel);
|
|
52
|
+
};
|
|
53
|
+
const grant = () => {
|
|
54
|
+
if (settled)
|
|
55
|
+
return;
|
|
56
|
+
settled = true;
|
|
57
|
+
cleanup();
|
|
58
|
+
resolve();
|
|
59
|
+
};
|
|
60
|
+
const cancel = () => {
|
|
61
|
+
if (settled)
|
|
62
|
+
return;
|
|
63
|
+
settled = true;
|
|
64
|
+
remove();
|
|
65
|
+
cleanup();
|
|
66
|
+
reject(new BBRadarApiError({ message: "BBRadar API request timed out or was cancelled while queued.", requestId, status: 408 }));
|
|
67
|
+
};
|
|
68
|
+
const timer = setTimeout(cancel, Math.max(1, deadline - Date.now()));
|
|
69
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
70
|
+
queue.push(grant);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
5
73
|
const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 60_000;
|
|
6
74
|
const MAX_UPSTREAM_RATE_LIMIT_DELAY_MS = 10 * 60_000;
|
|
7
75
|
const CHANGE_FEED_CONCURRENCY_LIMIT = 2;
|
|
@@ -14,6 +82,7 @@ const DEFAULT_PROGRAM_SWR_MS = 5 * 60_000;
|
|
|
14
82
|
const DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 3;
|
|
15
83
|
const DEFAULT_CIRCUIT_COOLDOWN_MS = 30_000;
|
|
16
84
|
const SNAPSHOT_START_MIN_TIMEOUT_MS = 60_000;
|
|
85
|
+
const MAX_QUEUED_REQUESTS = 100;
|
|
17
86
|
export class BBRadarApiError extends Error {
|
|
18
87
|
requestId;
|
|
19
88
|
upstreamRequestId;
|
|
@@ -23,6 +92,7 @@ export class BBRadarApiError extends Error {
|
|
|
23
92
|
cached;
|
|
24
93
|
coalesced;
|
|
25
94
|
cacheExpiresAt;
|
|
95
|
+
cacheAllowed;
|
|
26
96
|
constructor(params) {
|
|
27
97
|
super(params.message, { cause: params.cause });
|
|
28
98
|
this.name = "BBRadarApiError";
|
|
@@ -34,6 +104,7 @@ export class BBRadarApiError extends Error {
|
|
|
34
104
|
this.cached = params.cached ?? false;
|
|
35
105
|
this.coalesced = params.coalesced ?? false;
|
|
36
106
|
this.cacheExpiresAt = params.cacheExpiresAt;
|
|
107
|
+
this.cacheAllowed = params.cacheAllowed ?? true;
|
|
37
108
|
}
|
|
38
109
|
}
|
|
39
110
|
export class BBRadarClient {
|
|
@@ -41,6 +112,8 @@ export class BBRadarClient {
|
|
|
41
112
|
cache;
|
|
42
113
|
negativeCache;
|
|
43
114
|
inFlight = new Map();
|
|
115
|
+
sharedRequests = new Map();
|
|
116
|
+
requestContext = new AsyncLocalStorage();
|
|
44
117
|
endpointMetrics = new Map();
|
|
45
118
|
requestQueue = [];
|
|
46
119
|
endpointRequestQueues = new Map();
|
|
@@ -57,6 +130,24 @@ export class BBRadarClient {
|
|
|
57
130
|
this.cache = new TtlCache(config.cacheMaxEntries, config.cacheTtlMs, config.programStaleWhileRevalidateMs ?? DEFAULT_PROGRAM_SWR_MS);
|
|
58
131
|
this.negativeCache = new TtlCache(config.cacheMaxEntries, config.negativeCacheTtlMs);
|
|
59
132
|
}
|
|
133
|
+
async withRequestContext(signal, callback, snapshotStart = false) {
|
|
134
|
+
const controller = new AbortController();
|
|
135
|
+
const onAbort = () => controller.abort(signal.reason);
|
|
136
|
+
const timeout = setTimeout(() => controller.abort(new DOMException("Tool deadline exceeded.", "TimeoutError")), snapshotStart
|
|
137
|
+
? Math.max(this.config.requestTimeoutMs, SNAPSHOT_START_MIN_TIMEOUT_MS)
|
|
138
|
+
: this.config.requestTimeoutMs);
|
|
139
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
140
|
+
if (signal.aborted)
|
|
141
|
+
onAbort();
|
|
142
|
+
try {
|
|
143
|
+
return await this.requestContext.run(controller.signal, callback);
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
clearTimeout(timeout);
|
|
147
|
+
signal.removeEventListener("abort", onAbort);
|
|
148
|
+
controller.abort();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
60
151
|
diagnostics() {
|
|
61
152
|
const endpointEntries = [...this.endpointMetrics.entries()];
|
|
62
153
|
const totalRequests = endpointEntries.reduce((total, [, metric]) => total + metric.requests, 0);
|
|
@@ -268,6 +359,27 @@ export class BBRadarClient {
|
|
|
268
359
|
query
|
|
269
360
|
});
|
|
270
361
|
}
|
|
362
|
+
getHistoryChanges(query) {
|
|
363
|
+
return this.request({ path: "/pro/history/changes", query, cache: false });
|
|
364
|
+
}
|
|
365
|
+
getHistoryChangeDetail(changeId, query) {
|
|
366
|
+
return this.request({ path: `/pro/history/changes/${encodeURIComponent(changeId)}`, query, cache: false });
|
|
367
|
+
}
|
|
368
|
+
getProgramHistory(programId, query) {
|
|
369
|
+
return this.request({ path: `/pro/history/programs/${encodeProgramIdPath(programId)}`, query, cache: false });
|
|
370
|
+
}
|
|
371
|
+
getProgramActivity(programId) {
|
|
372
|
+
return this.request({ path: `/pro/activity/programs/${encodeProgramIdPath(programId)}`, cache: false });
|
|
373
|
+
}
|
|
374
|
+
getAnalyticsPlatformSeries(platform, query) {
|
|
375
|
+
return this.request({ path: `/pro/analytics/platforms/${encodeURIComponent(platform)}/series`, query, cache: false });
|
|
376
|
+
}
|
|
377
|
+
getAnalyticsProgramActivity(query) {
|
|
378
|
+
return this.request({ path: "/pro/analytics/programs/activity", query, cache: false });
|
|
379
|
+
}
|
|
380
|
+
getAnalyticsProgramDetail(programId, query) {
|
|
381
|
+
return this.request({ path: `/pro/analytics/programs/${encodeProgramIdPath(programId)}`, query, cache: false });
|
|
382
|
+
}
|
|
271
383
|
exportTargets(body) {
|
|
272
384
|
return this.request({
|
|
273
385
|
method: "POST",
|
|
@@ -278,8 +390,33 @@ export class BBRadarClient {
|
|
|
278
390
|
});
|
|
279
391
|
}
|
|
280
392
|
async request(options) {
|
|
393
|
+
const contextSignal = this.requestContext.getStore();
|
|
394
|
+
if (!options.signal || !contextSignal || options.signal === contextSignal) {
|
|
395
|
+
return this.requestWithSignal(options, options.signal ?? contextSignal);
|
|
396
|
+
}
|
|
397
|
+
// AbortSignal.any is unavailable on the earliest supported Node 20 releases.
|
|
398
|
+
// Remove both listeners after this caller settles, even for coalesced reads.
|
|
399
|
+
const controller = new AbortController();
|
|
400
|
+
const signals = [options.signal, contextSignal];
|
|
401
|
+
const listeners = signals.map((signal) => {
|
|
402
|
+
const abort = () => controller.abort(signal.reason);
|
|
403
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
404
|
+
if (signal.aborted)
|
|
405
|
+
abort();
|
|
406
|
+
return abort;
|
|
407
|
+
});
|
|
408
|
+
try {
|
|
409
|
+
return await this.requestWithSignal(options, controller.signal);
|
|
410
|
+
}
|
|
411
|
+
finally {
|
|
412
|
+
signals.forEach((signal, index) => signal.removeEventListener("abort", listeners[index]));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async requestWithSignal(options, signal) {
|
|
281
416
|
const method = options.method ?? "GET";
|
|
282
417
|
const requestId = randomUUID();
|
|
418
|
+
if (signal?.aborted)
|
|
419
|
+
throw new BBRadarApiError({ message: "BBRadar API request was cancelled or exceeded its tool deadline.", requestId, status: 408 });
|
|
283
420
|
const url = this.buildUrl(options.path, options.query);
|
|
284
421
|
const requestKey = stableStringify({
|
|
285
422
|
method,
|
|
@@ -299,7 +436,7 @@ export class BBRadarClient {
|
|
|
299
436
|
this.refreshInBackground(options, method, url, requestKey);
|
|
300
437
|
}
|
|
301
438
|
return {
|
|
302
|
-
...cached.value,
|
|
439
|
+
...publicPayload(cached.value),
|
|
303
440
|
requestId,
|
|
304
441
|
cached: true,
|
|
305
442
|
stale: cached.stale,
|
|
@@ -320,11 +457,11 @@ export class BBRadarClient {
|
|
|
320
457
|
}
|
|
321
458
|
if (coalesceable) {
|
|
322
459
|
const pending = this.inFlight.get(requestKey);
|
|
323
|
-
if (pending) {
|
|
460
|
+
if (pending && !this.sharedRequests.get(requestKey)?.controller.signal.aborted) {
|
|
324
461
|
try {
|
|
325
|
-
const result = (await pending);
|
|
462
|
+
const result = (await subscribeToRequest(pending, requestId, signal, this.sharedRequests.get(requestKey)));
|
|
326
463
|
return {
|
|
327
|
-
...result,
|
|
464
|
+
...publicPayload(result),
|
|
328
465
|
requestId,
|
|
329
466
|
cached: false,
|
|
330
467
|
coalesced: true
|
|
@@ -335,27 +472,20 @@ export class BBRadarClient {
|
|
|
335
472
|
}
|
|
336
473
|
}
|
|
337
474
|
}
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
try {
|
|
343
|
-
const result = await request;
|
|
344
|
-
if (cacheable) {
|
|
475
|
+
const shared = coalesceable ? { controller: new AbortController(), consumers: 0 } : undefined;
|
|
476
|
+
const requestOptions = { ...options, ...(shared ? { signal: shared.controller.signal } : signal ? { signal } : {}) };
|
|
477
|
+
const request = this.fetchFromApi(requestOptions, method, url, requestId).then((result) => {
|
|
478
|
+
if (cacheable && result.cacheAllowed !== false) {
|
|
345
479
|
this.cache.set(requestKey, result);
|
|
346
480
|
}
|
|
347
481
|
if (negativeCacheable) {
|
|
348
482
|
this.negativeCache.delete(requestKey);
|
|
349
483
|
}
|
|
350
|
-
return
|
|
351
|
-
|
|
352
|
-
requestId,
|
|
353
|
-
cached: false
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
catch (error) {
|
|
484
|
+
return result;
|
|
485
|
+
}).catch((error) => {
|
|
357
486
|
if (negativeCacheable &&
|
|
358
487
|
error instanceof BBRadarApiError &&
|
|
488
|
+
error.cacheAllowed &&
|
|
359
489
|
error.status !== undefined &&
|
|
360
490
|
isNegativeCacheStatus(error.status)) {
|
|
361
491
|
this.negativeCache.set(requestKey, {
|
|
@@ -367,14 +497,21 @@ export class BBRadarClient {
|
|
|
367
497
|
});
|
|
368
498
|
}
|
|
369
499
|
throw error;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (coalesceable) {
|
|
500
|
+
}).finally(() => {
|
|
501
|
+
if (coalesceable && this.inFlight.get(requestKey) === request) {
|
|
373
502
|
this.inFlight.delete(requestKey);
|
|
503
|
+
this.sharedRequests.delete(requestKey);
|
|
374
504
|
}
|
|
505
|
+
});
|
|
506
|
+
if (coalesceable) {
|
|
507
|
+
this.inFlight.set(requestKey, request);
|
|
508
|
+
this.sharedRequests.set(requestKey, shared);
|
|
375
509
|
}
|
|
510
|
+
const result = await subscribeToRequest(request, requestId, signal, shared);
|
|
511
|
+
return { ...publicPayload(result), requestId, cached: false };
|
|
376
512
|
}
|
|
377
513
|
async fetchFromApi(options, method, url, requestId) {
|
|
514
|
+
const deadline = Date.now() + (options.timeoutMs ?? this.config.requestTimeoutMs);
|
|
378
515
|
const serializedBody = options.body === undefined ? undefined : JSON.stringify(options.body);
|
|
379
516
|
if (serializedBody !== undefined) {
|
|
380
517
|
const bodyBytes = Buffer.byteLength(serializedBody, "utf8");
|
|
@@ -389,11 +526,20 @@ export class BBRadarClient {
|
|
|
389
526
|
}
|
|
390
527
|
return this.withRequestSlot(options.path, async () => {
|
|
391
528
|
const rateLimitBucket = rateLimitEndpointGroup(options.path);
|
|
529
|
+
if (options.signal?.aborted)
|
|
530
|
+
throw new BBRadarApiError({ message: "BBRadar API request was cancelled.", requestId, status: 408 });
|
|
392
531
|
this.assertCircuitClosed(requestId, rateLimitBucket);
|
|
393
532
|
this.reserveUpstreamRequest(requestId, rateLimitBucket);
|
|
394
533
|
const startedAt = Date.now();
|
|
395
534
|
const controller = new AbortController();
|
|
396
|
-
const
|
|
535
|
+
const remainingMs = deadline - Date.now();
|
|
536
|
+
if (remainingMs <= 0)
|
|
537
|
+
throw new BBRadarApiError({ message: "BBRadar API request timed out while queued.", requestId, status: 408 });
|
|
538
|
+
const timeout = setTimeout(() => controller.abort(), remainingMs);
|
|
539
|
+
const onAbort = () => controller.abort();
|
|
540
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
541
|
+
if (options.signal?.aborted)
|
|
542
|
+
controller.abort();
|
|
397
543
|
let status;
|
|
398
544
|
let failed = true;
|
|
399
545
|
try {
|
|
@@ -407,6 +553,7 @@ export class BBRadarClient {
|
|
|
407
553
|
}
|
|
408
554
|
const response = await fetch(url, init);
|
|
409
555
|
status = response.status;
|
|
556
|
+
const noStore = /(?:^|,)\s*(?:no-store|no-cache|private)\b/i.test(response.headers.get("cache-control") ?? "");
|
|
410
557
|
this.observeCircuitResponse(rateLimitBucket, response.status);
|
|
411
558
|
this.observeUpstreamRateLimit(rateLimitBucket, response.headers, response.status);
|
|
412
559
|
const upstreamRequestId = extractRequestId(response.headers);
|
|
@@ -418,7 +565,8 @@ export class BBRadarClient {
|
|
|
418
565
|
requestId,
|
|
419
566
|
status: response.status,
|
|
420
567
|
detail: extractObjectField(data, "detail"),
|
|
421
|
-
errors: extractObjectField(data, "errors")
|
|
568
|
+
errors: extractObjectField(data, "errors"),
|
|
569
|
+
cacheAllowed: !noStore
|
|
422
570
|
};
|
|
423
571
|
if (upstreamRequestId) {
|
|
424
572
|
params.upstreamRequestId = upstreamRequestId;
|
|
@@ -428,7 +576,9 @@ export class BBRadarClient {
|
|
|
428
576
|
const result = {
|
|
429
577
|
data: data,
|
|
430
578
|
status: response.status,
|
|
431
|
-
fetchedAt: new Date().toISOString()
|
|
579
|
+
fetchedAt: new Date().toISOString(),
|
|
580
|
+
noStore,
|
|
581
|
+
cacheAllowed: !noStore
|
|
432
582
|
};
|
|
433
583
|
if (upstreamRequestId) {
|
|
434
584
|
result.upstreamRequestId = upstreamRequestId;
|
|
@@ -440,6 +590,12 @@ export class BBRadarClient {
|
|
|
440
590
|
if (error instanceof BBRadarApiError) {
|
|
441
591
|
throw error;
|
|
442
592
|
}
|
|
593
|
+
if (options.signal?.aborted) {
|
|
594
|
+
const timedOut = options.signal.reason instanceof Error && options.signal.reason.name === "TimeoutError";
|
|
595
|
+
if (timedOut)
|
|
596
|
+
this.observeCircuitFailure(rateLimitBucket);
|
|
597
|
+
throw new BBRadarApiError({ message: timedOut ? "BBRadar API tool deadline exceeded." : "BBRadar API request was cancelled.", requestId, status: 408 });
|
|
598
|
+
}
|
|
443
599
|
this.observeCircuitFailure(rateLimitBucket);
|
|
444
600
|
throw new BBRadarApiError({
|
|
445
601
|
message: error instanceof Error && error.name === "AbortError" ? "BBRadar API request timed out." : "BBRadar API request failed.",
|
|
@@ -449,9 +605,10 @@ export class BBRadarClient {
|
|
|
449
605
|
}
|
|
450
606
|
finally {
|
|
451
607
|
clearTimeout(timeout);
|
|
608
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
452
609
|
this.recordEndpointMetric(options.path, Date.now() - startedAt, status, failed);
|
|
453
610
|
}
|
|
454
|
-
});
|
|
611
|
+
}, deadline, requestId, options.signal);
|
|
455
612
|
}
|
|
456
613
|
refreshInBackground(options, method, url, requestKey) {
|
|
457
614
|
if (this.inFlight.has(requestKey)) {
|
|
@@ -461,7 +618,12 @@ export class BBRadarClient {
|
|
|
461
618
|
this.inFlight.set(requestKey, refresh);
|
|
462
619
|
void refresh
|
|
463
620
|
.then((result) => {
|
|
464
|
-
|
|
621
|
+
if (result.cacheAllowed !== false) {
|
|
622
|
+
this.cache.set(requestKey, result);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
this.cache.delete(requestKey);
|
|
626
|
+
}
|
|
465
627
|
this.negativeCache.delete(requestKey);
|
|
466
628
|
})
|
|
467
629
|
.catch(() => undefined)
|
|
@@ -471,21 +633,21 @@ export class BBRadarClient {
|
|
|
471
633
|
}
|
|
472
634
|
});
|
|
473
635
|
}
|
|
474
|
-
async withRequestSlot(path, callback) {
|
|
636
|
+
async withRequestSlot(path, callback, deadline, requestId, signal) {
|
|
475
637
|
const endpointGroup = concurrencyEndpointGroup(path);
|
|
476
638
|
if (endpointGroup === undefined) {
|
|
477
|
-
return this.withGlobalRequestSlot(callback);
|
|
639
|
+
return this.withGlobalRequestSlot(callback, deadline, requestId, signal);
|
|
478
640
|
}
|
|
479
641
|
const endpointLimit = endpointConcurrencyLimit(endpointGroup);
|
|
480
642
|
if (endpointLimit === undefined) {
|
|
481
|
-
return this.withGlobalRequestSlot(callback);
|
|
643
|
+
return this.withGlobalRequestSlot(callback, deadline, requestId, signal);
|
|
482
644
|
}
|
|
483
|
-
return this.withEndpointRequestSlot(endpointGroup, endpointLimit, () => this.withGlobalRequestSlot(callback));
|
|
645
|
+
return this.withEndpointRequestSlot(endpointGroup, endpointLimit, () => this.withGlobalRequestSlot(callback, deadline, requestId, signal), deadline, requestId, signal);
|
|
484
646
|
}
|
|
485
|
-
async withGlobalRequestSlot(callback) {
|
|
647
|
+
async withGlobalRequestSlot(callback, deadline, requestId, signal) {
|
|
486
648
|
const limit = Math.max(1, this.config.maxConcurrentRequests);
|
|
487
649
|
if (this.activeRequests >= limit) {
|
|
488
|
-
await
|
|
650
|
+
await waitForRequestSlot(this.requestQueue, deadline, requestId, signal);
|
|
489
651
|
}
|
|
490
652
|
else {
|
|
491
653
|
this.activeRequests += 1;
|
|
@@ -503,14 +665,12 @@ export class BBRadarClient {
|
|
|
503
665
|
}
|
|
504
666
|
}
|
|
505
667
|
}
|
|
506
|
-
async withEndpointRequestSlot(endpointGroup, limit, callback) {
|
|
668
|
+
async withEndpointRequestSlot(endpointGroup, limit, callback, deadline, requestId, signal) {
|
|
507
669
|
const active = this.endpointActiveRequests.get(endpointGroup) ?? 0;
|
|
508
670
|
if (active >= limit) {
|
|
509
671
|
const queue = this.endpointRequestQueues.get(endpointGroup) ?? [];
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
this.endpointRequestQueues.set(endpointGroup, queue);
|
|
513
|
-
});
|
|
672
|
+
this.endpointRequestQueues.set(endpointGroup, queue);
|
|
673
|
+
await waitForRequestSlot(queue, deadline, requestId, signal);
|
|
514
674
|
}
|
|
515
675
|
else {
|
|
516
676
|
this.endpointActiveRequests.set(endpointGroup, active + 1);
|
|
@@ -800,7 +960,9 @@ export class BBRadarClient {
|
|
|
800
960
|
};
|
|
801
961
|
}
|
|
802
962
|
buildUrl(path, query) {
|
|
803
|
-
const
|
|
963
|
+
const base = this.config.apiBaseUrl.replace(/\/+$/, "");
|
|
964
|
+
const prefix = /\/v1$/i.test(base) ? base : `${base}/v1`;
|
|
965
|
+
const url = new URL(`${prefix}${path}`);
|
|
804
966
|
for (const [key, value] of Object.entries(query ?? {})) {
|
|
805
967
|
if (value === undefined || value === null) {
|
|
806
968
|
continue;
|
|
@@ -905,6 +1067,16 @@ function toIsoString(timestamp) {
|
|
|
905
1067
|
return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
|
|
906
1068
|
}
|
|
907
1069
|
function metricEndpoint(path) {
|
|
1070
|
+
if (/^\/pro\/history\/changes\/.+/.test(path))
|
|
1071
|
+
return "/pro/history/changes/:change_id";
|
|
1072
|
+
if (/^\/pro\/history\/programs\/.+/.test(path))
|
|
1073
|
+
return "/pro/history/programs/:program_id";
|
|
1074
|
+
if (/^\/pro\/activity\/programs\/.+/.test(path))
|
|
1075
|
+
return "/pro/activity/programs/:program_id";
|
|
1076
|
+
if (/^\/pro\/analytics\/platforms\/.+\/series$/.test(path))
|
|
1077
|
+
return "/pro/analytics/platforms/:platform/series";
|
|
1078
|
+
if (/^\/pro\/analytics\/programs\/.+/.test(path) && path !== "/pro/analytics/programs/activity")
|
|
1079
|
+
return "/pro/analytics/programs/:program_id";
|
|
908
1080
|
if (/^\/pro\/programs\/.+\/targets$/.test(path)) {
|
|
909
1081
|
return "/pro/programs/:program_id/targets";
|
|
910
1082
|
}
|
|
@@ -917,7 +1089,7 @@ function metricEndpoint(path) {
|
|
|
917
1089
|
return path;
|
|
918
1090
|
}
|
|
919
1091
|
function rateLimitEndpointGroup(path) {
|
|
920
|
-
if (path === "/pro/targets/changes") {
|
|
1092
|
+
if (path === "/pro/targets/changes" || /^\/pro\/(?:history|activity|analytics)\//.test(path)) {
|
|
921
1093
|
return "change_feeds";
|
|
922
1094
|
}
|
|
923
1095
|
if (path === "/pro/repositories/changes") {
|
|
@@ -941,7 +1113,7 @@ function rateLimitEndpointGroup(path) {
|
|
|
941
1113
|
return "other";
|
|
942
1114
|
}
|
|
943
1115
|
function concurrencyEndpointGroup(path) {
|
|
944
|
-
if (path === "/pro/targets/changes" || path === "/pro/repositories/changes") {
|
|
1116
|
+
if (path === "/pro/targets/changes" || path === "/pro/repositories/changes" || /^\/pro\/(?:history|activity|analytics)\//.test(path)) {
|
|
945
1117
|
return "change_feeds";
|
|
946
1118
|
}
|
|
947
1119
|
if (path === "/pro/targets/export") {
|
|
@@ -1073,6 +1245,7 @@ function cloneApiError(error, requestId, coalesced = error instanceof BBRadarApi
|
|
|
1073
1245
|
cached: error.cached,
|
|
1074
1246
|
coalesced,
|
|
1075
1247
|
cacheExpiresAt: error.cacheExpiresAt,
|
|
1248
|
+
cacheAllowed: error.cacheAllowed,
|
|
1076
1249
|
cause: error
|
|
1077
1250
|
});
|
|
1078
1251
|
}
|