@bbradar/mcp 0.2.5 → 0.3.0

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.
@@ -1,7 +1,76 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import { requiresActivityQuery } from "./activityContract.js";
3
+ import { AsyncLocalStorage } from "node:async_hooks";
2
4
  import { TtlCache } from "./cache.js";
3
5
  import { stableStringify } from "./json.js";
4
6
  import { encodeProgramIdPath } from "./url.js";
7
+ function publicPayload(payload) {
8
+ const { cacheAllowed: _cacheAllowed, ...publicFields } = payload;
9
+ return publicFields;
10
+ }
11
+ function subscribeToRequest(promise, requestId, signal, shared) {
12
+ if (shared)
13
+ shared.consumers += 1;
14
+ return new Promise((resolve, reject) => {
15
+ let settled = false;
16
+ const finish = (callback) => {
17
+ if (settled)
18
+ return;
19
+ settled = true;
20
+ signal?.removeEventListener("abort", cancel);
21
+ if (shared)
22
+ shared.consumers -= 1;
23
+ callback();
24
+ };
25
+ const cancel = () => finish(() => {
26
+ if (shared?.consumers === 0)
27
+ shared.controller.abort(signal?.reason);
28
+ reject(new BBRadarApiError({ message: "BBRadar API request was cancelled or exceeded its tool deadline.", requestId, status: 408 }));
29
+ });
30
+ signal?.addEventListener("abort", cancel, { once: true });
31
+ promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
32
+ if (signal?.aborted)
33
+ cancel();
34
+ });
35
+ }
36
+ function waitForRequestSlot(queue, deadline, requestId, signal) {
37
+ if (queue.length >= MAX_QUEUED_REQUESTS) {
38
+ throw new BBRadarApiError({ message: "BBRadar API request queue is full.", requestId, status: 503 });
39
+ }
40
+ if (deadline <= Date.now() || signal?.aborted) {
41
+ throw new BBRadarApiError({ message: "BBRadar API request timed out or was cancelled while queued.", requestId, status: 408 });
42
+ }
43
+ return new Promise((resolve, reject) => {
44
+ let settled = false;
45
+ const remove = () => {
46
+ const index = queue.indexOf(grant);
47
+ if (index >= 0)
48
+ queue.splice(index, 1);
49
+ };
50
+ const cleanup = () => {
51
+ clearTimeout(timer);
52
+ signal?.removeEventListener("abort", cancel);
53
+ };
54
+ const grant = () => {
55
+ if (settled)
56
+ return;
57
+ settled = true;
58
+ cleanup();
59
+ resolve();
60
+ };
61
+ const cancel = () => {
62
+ if (settled)
63
+ return;
64
+ settled = true;
65
+ remove();
66
+ cleanup();
67
+ reject(new BBRadarApiError({ message: "BBRadar API request timed out or was cancelled while queued.", requestId, status: 408 }));
68
+ };
69
+ const timer = setTimeout(cancel, Math.max(1, deadline - Date.now()));
70
+ signal?.addEventListener("abort", cancel, { once: true });
71
+ queue.push(grant);
72
+ });
73
+ }
5
74
  const DEFAULT_RATE_LIMIT_COOLDOWN_MS = 60_000;
6
75
  const MAX_UPSTREAM_RATE_LIMIT_DELAY_MS = 10 * 60_000;
7
76
  const CHANGE_FEED_CONCURRENCY_LIMIT = 2;
@@ -14,6 +83,7 @@ const DEFAULT_PROGRAM_SWR_MS = 5 * 60_000;
14
83
  const DEFAULT_CIRCUIT_FAILURE_THRESHOLD = 3;
15
84
  const DEFAULT_CIRCUIT_COOLDOWN_MS = 30_000;
16
85
  const SNAPSHOT_START_MIN_TIMEOUT_MS = 60_000;
86
+ const MAX_QUEUED_REQUESTS = 100;
17
87
  export class BBRadarApiError extends Error {
18
88
  requestId;
19
89
  upstreamRequestId;
@@ -23,6 +93,7 @@ export class BBRadarApiError extends Error {
23
93
  cached;
24
94
  coalesced;
25
95
  cacheExpiresAt;
96
+ cacheAllowed;
26
97
  constructor(params) {
27
98
  super(params.message, { cause: params.cause });
28
99
  this.name = "BBRadarApiError";
@@ -34,6 +105,7 @@ export class BBRadarApiError extends Error {
34
105
  this.cached = params.cached ?? false;
35
106
  this.coalesced = params.coalesced ?? false;
36
107
  this.cacheExpiresAt = params.cacheExpiresAt;
108
+ this.cacheAllowed = params.cacheAllowed ?? true;
37
109
  }
38
110
  }
39
111
  export class BBRadarClient {
@@ -41,6 +113,8 @@ export class BBRadarClient {
41
113
  cache;
42
114
  negativeCache;
43
115
  inFlight = new Map();
116
+ sharedRequests = new Map();
117
+ requestContext = new AsyncLocalStorage();
44
118
  endpointMetrics = new Map();
45
119
  requestQueue = [];
46
120
  endpointRequestQueues = new Map();
@@ -57,6 +131,24 @@ export class BBRadarClient {
57
131
  this.cache = new TtlCache(config.cacheMaxEntries, config.cacheTtlMs, config.programStaleWhileRevalidateMs ?? DEFAULT_PROGRAM_SWR_MS);
58
132
  this.negativeCache = new TtlCache(config.cacheMaxEntries, config.negativeCacheTtlMs);
59
133
  }
134
+ async withRequestContext(signal, callback, snapshotStart = false) {
135
+ const controller = new AbortController();
136
+ const onAbort = () => controller.abort(signal.reason);
137
+ const timeout = setTimeout(() => controller.abort(new DOMException("Tool deadline exceeded.", "TimeoutError")), snapshotStart
138
+ ? Math.max(this.config.requestTimeoutMs, SNAPSHOT_START_MIN_TIMEOUT_MS)
139
+ : this.config.requestTimeoutMs);
140
+ signal.addEventListener("abort", onAbort, { once: true });
141
+ if (signal.aborted)
142
+ onAbort();
143
+ try {
144
+ return await this.requestContext.run(controller.signal, callback);
145
+ }
146
+ finally {
147
+ clearTimeout(timeout);
148
+ signal.removeEventListener("abort", onAbort);
149
+ controller.abort();
150
+ }
151
+ }
60
152
  diagnostics() {
61
153
  const endpointEntries = [...this.endpointMetrics.entries()];
62
154
  const totalRequests = endpointEntries.reduce((total, [, metric]) => total + metric.requests, 0);
@@ -265,9 +357,46 @@ export class BBRadarClient {
265
357
  getOpportunities(level, query) {
266
358
  return this.request({
267
359
  path: `/pro/opportunities/${encodeURIComponent(level)}`,
268
- query
360
+ query,
361
+ ...snapshotAwareReadControls(query, {}, true, this.config.requestTimeoutMs)
269
362
  });
270
363
  }
364
+ getHistoryChanges(query) {
365
+ return this.request({ path: "/pro/history/changes", query, cache: false });
366
+ }
367
+ getHistoryChangeDetail(changeId, query) {
368
+ return this.request({ path: `/pro/history/changes/${encodeURIComponent(changeId)}`, query, cache: false });
369
+ }
370
+ getProgramHistory(programId, query) {
371
+ return this.request({ path: `/pro/history/programs/${encodeProgramIdPath(programId)}`, query, cache: false });
372
+ }
373
+ getProgramActivity(programId) {
374
+ return this.request({ path: `/pro/activity/programs/${encodeProgramIdPath(programId)}`, cache: false });
375
+ }
376
+ getActivityCapabilities(query = {}) {
377
+ return this.request({ path: "/pro/activity/capabilities", query, cache: false });
378
+ }
379
+ getActivitySnapshot() {
380
+ return this.request({ path: "/pro/activity/snapshot", cache: false });
381
+ }
382
+ getActivityRankings(query) {
383
+ return this.request({ path: "/pro/activity/rankings", query, cache: false });
384
+ }
385
+ getActivityChangeDetail(eventId, query) {
386
+ return this.request({ path: `/pro/activity/events/${encodeURIComponent(eventId)}`, query, cache: false });
387
+ }
388
+ getProgramPayouts(programId, query = {}) {
389
+ return this.request({ path: `/pro/payouts/programs/${encodeProgramIdPath(programId)}`, query, cache: false });
390
+ }
391
+ getAnalyticsPlatformSeries(platform, query) {
392
+ return this.request({ path: `/pro/analytics/platforms/${encodeURIComponent(platform)}/series`, query, cache: false });
393
+ }
394
+ getAnalyticsProgramActivity(query) {
395
+ return this.request({ path: "/pro/analytics/programs/activity", query, cache: false });
396
+ }
397
+ getAnalyticsProgramDetail(programId, query) {
398
+ return this.request({ path: `/pro/analytics/programs/${encodeProgramIdPath(programId)}`, query, cache: false });
399
+ }
271
400
  exportTargets(body) {
272
401
  return this.request({
273
402
  method: "POST",
@@ -278,8 +407,33 @@ export class BBRadarClient {
278
407
  });
279
408
  }
280
409
  async request(options) {
410
+ const contextSignal = this.requestContext.getStore();
411
+ if (!options.signal || !contextSignal || options.signal === contextSignal) {
412
+ return this.requestWithSignal(options, options.signal ?? contextSignal);
413
+ }
414
+ // AbortSignal.any is unavailable on the earliest supported Node 20 releases.
415
+ // Remove both listeners after this caller settles, even for coalesced reads.
416
+ const controller = new AbortController();
417
+ const signals = [options.signal, contextSignal];
418
+ const listeners = signals.map((signal) => {
419
+ const abort = () => controller.abort(signal.reason);
420
+ signal.addEventListener("abort", abort, { once: true });
421
+ if (signal.aborted)
422
+ abort();
423
+ return abort;
424
+ });
425
+ try {
426
+ return await this.requestWithSignal(options, controller.signal);
427
+ }
428
+ finally {
429
+ signals.forEach((signal, index) => signal.removeEventListener("abort", listeners[index]));
430
+ }
431
+ }
432
+ async requestWithSignal(options, signal) {
281
433
  const method = options.method ?? "GET";
282
434
  const requestId = randomUUID();
435
+ if (signal?.aborted)
436
+ throw new BBRadarApiError({ message: "BBRadar API request was cancelled or exceeded its tool deadline.", requestId, status: 408 });
283
437
  const url = this.buildUrl(options.path, options.query);
284
438
  const requestKey = stableStringify({
285
439
  method,
@@ -299,7 +453,7 @@ export class BBRadarClient {
299
453
  this.refreshInBackground(options, method, url, requestKey);
300
454
  }
301
455
  return {
302
- ...cached.value,
456
+ ...publicPayload(cached.value),
303
457
  requestId,
304
458
  cached: true,
305
459
  stale: cached.stale,
@@ -320,11 +474,11 @@ export class BBRadarClient {
320
474
  }
321
475
  if (coalesceable) {
322
476
  const pending = this.inFlight.get(requestKey);
323
- if (pending) {
477
+ if (pending && !this.sharedRequests.get(requestKey)?.controller.signal.aborted) {
324
478
  try {
325
- const result = (await pending);
479
+ const result = (await subscribeToRequest(pending, requestId, signal, this.sharedRequests.get(requestKey)));
326
480
  return {
327
- ...result,
481
+ ...publicPayload(result),
328
482
  requestId,
329
483
  cached: false,
330
484
  coalesced: true
@@ -335,27 +489,20 @@ export class BBRadarClient {
335
489
  }
336
490
  }
337
491
  }
338
- const request = this.fetchFromApi(options, method, url, requestId);
339
- if (coalesceable) {
340
- this.inFlight.set(requestKey, request);
341
- }
342
- try {
343
- const result = await request;
344
- if (cacheable) {
492
+ const shared = coalesceable ? { controller: new AbortController(), consumers: 0 } : undefined;
493
+ const requestOptions = { ...options, ...(shared ? { signal: shared.controller.signal } : signal ? { signal } : {}) };
494
+ const request = this.fetchFromApi(requestOptions, method, url, requestId).then((result) => {
495
+ if (cacheable && result.cacheAllowed !== false) {
345
496
  this.cache.set(requestKey, result);
346
497
  }
347
498
  if (negativeCacheable) {
348
499
  this.negativeCache.delete(requestKey);
349
500
  }
350
- return {
351
- ...result,
352
- requestId,
353
- cached: false
354
- };
355
- }
356
- catch (error) {
501
+ return result;
502
+ }).catch((error) => {
357
503
  if (negativeCacheable &&
358
504
  error instanceof BBRadarApiError &&
505
+ error.cacheAllowed &&
359
506
  error.status !== undefined &&
360
507
  isNegativeCacheStatus(error.status)) {
361
508
  this.negativeCache.set(requestKey, {
@@ -367,14 +514,21 @@ export class BBRadarClient {
367
514
  });
368
515
  }
369
516
  throw error;
370
- }
371
- finally {
372
- if (coalesceable) {
517
+ }).finally(() => {
518
+ if (coalesceable && this.inFlight.get(requestKey) === request) {
373
519
  this.inFlight.delete(requestKey);
520
+ this.sharedRequests.delete(requestKey);
374
521
  }
522
+ });
523
+ if (coalesceable) {
524
+ this.inFlight.set(requestKey, request);
525
+ this.sharedRequests.set(requestKey, shared);
375
526
  }
527
+ const result = await subscribeToRequest(request, requestId, signal, shared);
528
+ return { ...publicPayload(result), requestId, cached: false };
376
529
  }
377
530
  async fetchFromApi(options, method, url, requestId) {
531
+ const deadline = Date.now() + (options.timeoutMs ?? this.config.requestTimeoutMs);
378
532
  const serializedBody = options.body === undefined ? undefined : JSON.stringify(options.body);
379
533
  if (serializedBody !== undefined) {
380
534
  const bodyBytes = Buffer.byteLength(serializedBody, "utf8");
@@ -387,13 +541,23 @@ export class BBRadarClient {
387
541
  });
388
542
  }
389
543
  }
390
- return this.withRequestSlot(options.path, async () => {
391
- const rateLimitBucket = rateLimitEndpointGroup(options.path);
392
- this.assertCircuitClosed(requestId, rateLimitBucket);
544
+ return this.withRequestSlot(options.path, options.query, async () => {
545
+ const rateLimitBucket = rateLimitEndpointGroup(options.path, options.query);
546
+ const circuitBucket = activityCircuitBucket(options.path, options.query) ?? rateLimitBucket;
547
+ if (options.signal?.aborted)
548
+ throw new BBRadarApiError({ message: "BBRadar API request was cancelled.", requestId, status: 408 });
549
+ this.assertCircuitClosed(requestId, circuitBucket);
393
550
  this.reserveUpstreamRequest(requestId, rateLimitBucket);
394
551
  const startedAt = Date.now();
395
552
  const controller = new AbortController();
396
- const timeout = setTimeout(() => controller.abort(), options.timeoutMs ?? this.config.requestTimeoutMs);
553
+ const remainingMs = deadline - Date.now();
554
+ if (remainingMs <= 0)
555
+ throw new BBRadarApiError({ message: "BBRadar API request timed out while queued.", requestId, status: 408 });
556
+ const timeout = setTimeout(() => controller.abort(), remainingMs);
557
+ const onAbort = () => controller.abort();
558
+ options.signal?.addEventListener("abort", onAbort, { once: true });
559
+ if (options.signal?.aborted)
560
+ controller.abort();
397
561
  let status;
398
562
  let failed = true;
399
563
  try {
@@ -407,7 +571,8 @@ export class BBRadarClient {
407
571
  }
408
572
  const response = await fetch(url, init);
409
573
  status = response.status;
410
- this.observeCircuitResponse(rateLimitBucket, response.status);
574
+ const noStore = /(?:^|,)\s*(?:no-store|no-cache|private)\b/i.test(response.headers.get("cache-control") ?? "");
575
+ this.observeCircuitResponse(circuitBucket, response.status);
411
576
  this.observeUpstreamRateLimit(rateLimitBucket, response.headers, response.status);
412
577
  const upstreamRequestId = extractRequestId(response.headers);
413
578
  const data = await parseResponseBody(response);
@@ -418,7 +583,8 @@ export class BBRadarClient {
418
583
  requestId,
419
584
  status: response.status,
420
585
  detail: extractObjectField(data, "detail"),
421
- errors: extractObjectField(data, "errors")
586
+ errors: extractObjectField(data, "errors"),
587
+ cacheAllowed: !noStore
422
588
  };
423
589
  if (upstreamRequestId) {
424
590
  params.upstreamRequestId = upstreamRequestId;
@@ -428,7 +594,9 @@ export class BBRadarClient {
428
594
  const result = {
429
595
  data: data,
430
596
  status: response.status,
431
- fetchedAt: new Date().toISOString()
597
+ fetchedAt: new Date().toISOString(),
598
+ noStore,
599
+ cacheAllowed: !noStore
432
600
  };
433
601
  if (upstreamRequestId) {
434
602
  result.upstreamRequestId = upstreamRequestId;
@@ -440,7 +608,13 @@ export class BBRadarClient {
440
608
  if (error instanceof BBRadarApiError) {
441
609
  throw error;
442
610
  }
443
- this.observeCircuitFailure(rateLimitBucket);
611
+ if (options.signal?.aborted) {
612
+ const timedOut = options.signal.reason instanceof Error && options.signal.reason.name === "TimeoutError";
613
+ if (timedOut)
614
+ this.observeCircuitFailure(circuitBucket);
615
+ throw new BBRadarApiError({ message: timedOut ? "BBRadar API tool deadline exceeded." : "BBRadar API request was cancelled.", requestId, status: 408 });
616
+ }
617
+ this.observeCircuitFailure(circuitBucket);
444
618
  throw new BBRadarApiError({
445
619
  message: error instanceof Error && error.name === "AbortError" ? "BBRadar API request timed out." : "BBRadar API request failed.",
446
620
  requestId,
@@ -449,9 +623,10 @@ export class BBRadarClient {
449
623
  }
450
624
  finally {
451
625
  clearTimeout(timeout);
626
+ options.signal?.removeEventListener("abort", onAbort);
452
627
  this.recordEndpointMetric(options.path, Date.now() - startedAt, status, failed);
453
628
  }
454
- });
629
+ }, deadline, requestId, options.signal);
455
630
  }
456
631
  refreshInBackground(options, method, url, requestKey) {
457
632
  if (this.inFlight.has(requestKey)) {
@@ -461,7 +636,12 @@ export class BBRadarClient {
461
636
  this.inFlight.set(requestKey, refresh);
462
637
  void refresh
463
638
  .then((result) => {
464
- this.cache.set(requestKey, result);
639
+ if (result.cacheAllowed !== false) {
640
+ this.cache.set(requestKey, result);
641
+ }
642
+ else {
643
+ this.cache.delete(requestKey);
644
+ }
465
645
  this.negativeCache.delete(requestKey);
466
646
  })
467
647
  .catch(() => undefined)
@@ -471,21 +651,21 @@ export class BBRadarClient {
471
651
  }
472
652
  });
473
653
  }
474
- async withRequestSlot(path, callback) {
475
- const endpointGroup = concurrencyEndpointGroup(path);
654
+ async withRequestSlot(path, query, callback, deadline, requestId, signal) {
655
+ const endpointGroup = concurrencyEndpointGroup(path, query);
476
656
  if (endpointGroup === undefined) {
477
- return this.withGlobalRequestSlot(callback);
657
+ return this.withGlobalRequestSlot(callback, deadline, requestId, signal);
478
658
  }
479
659
  const endpointLimit = endpointConcurrencyLimit(endpointGroup);
480
660
  if (endpointLimit === undefined) {
481
- return this.withGlobalRequestSlot(callback);
661
+ return this.withGlobalRequestSlot(callback, deadline, requestId, signal);
482
662
  }
483
- return this.withEndpointRequestSlot(endpointGroup, endpointLimit, () => this.withGlobalRequestSlot(callback));
663
+ return this.withEndpointRequestSlot(endpointGroup, endpointLimit, () => this.withGlobalRequestSlot(callback, deadline, requestId, signal), deadline, requestId, signal);
484
664
  }
485
- async withGlobalRequestSlot(callback) {
665
+ async withGlobalRequestSlot(callback, deadline, requestId, signal) {
486
666
  const limit = Math.max(1, this.config.maxConcurrentRequests);
487
667
  if (this.activeRequests >= limit) {
488
- await new Promise((resolve) => this.requestQueue.push(resolve));
668
+ await waitForRequestSlot(this.requestQueue, deadline, requestId, signal);
489
669
  }
490
670
  else {
491
671
  this.activeRequests += 1;
@@ -503,14 +683,12 @@ export class BBRadarClient {
503
683
  }
504
684
  }
505
685
  }
506
- async withEndpointRequestSlot(endpointGroup, limit, callback) {
686
+ async withEndpointRequestSlot(endpointGroup, limit, callback, deadline, requestId, signal) {
507
687
  const active = this.endpointActiveRequests.get(endpointGroup) ?? 0;
508
688
  if (active >= limit) {
509
689
  const queue = this.endpointRequestQueues.get(endpointGroup) ?? [];
510
- await new Promise((resolve) => {
511
- queue.push(resolve);
512
- this.endpointRequestQueues.set(endpointGroup, queue);
513
- });
690
+ this.endpointRequestQueues.set(endpointGroup, queue);
691
+ await waitForRequestSlot(queue, deadline, requestId, signal);
514
692
  }
515
693
  else {
516
694
  this.endpointActiveRequests.set(endpointGroup, active + 1);
@@ -800,7 +978,9 @@ export class BBRadarClient {
800
978
  };
801
979
  }
802
980
  buildUrl(path, query) {
803
- const url = new URL(`${this.config.apiBaseUrl}${path}`);
981
+ const base = this.config.apiBaseUrl.replace(/\/+$/, "");
982
+ const prefix = /\/v1$/i.test(base) ? base : `${base}/v1`;
983
+ const url = new URL(`${prefix}${path}`);
804
984
  for (const [key, value] of Object.entries(query ?? {})) {
805
985
  if (value === undefined || value === null) {
806
986
  continue;
@@ -905,6 +1085,20 @@ function toIsoString(timestamp) {
905
1085
  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
906
1086
  }
907
1087
  function metricEndpoint(path) {
1088
+ if (/^\/pro\/history\/changes\/.+/.test(path))
1089
+ return "/pro/history/changes/:change_id";
1090
+ if (/^\/pro\/history\/programs\/.+/.test(path))
1091
+ return "/pro/history/programs/:program_id";
1092
+ if (/^\/pro\/activity\/programs\/.+/.test(path))
1093
+ return "/pro/activity/programs/:program_id";
1094
+ if (/^\/pro\/activity\/events\/.+/.test(path))
1095
+ return "/pro/activity/events/:event_id";
1096
+ if (/^\/pro\/payouts\/programs\/.+/.test(path))
1097
+ return "/pro/payouts/programs/:program_id";
1098
+ if (/^\/pro\/analytics\/platforms\/.+\/series$/.test(path))
1099
+ return "/pro/analytics/platforms/:platform/series";
1100
+ if (/^\/pro\/analytics\/programs\/.+/.test(path) && path !== "/pro/analytics/programs/activity")
1101
+ return "/pro/analytics/programs/:program_id";
908
1102
  if (/^\/pro\/programs\/.+\/targets$/.test(path)) {
909
1103
  return "/pro/programs/:program_id/targets";
910
1104
  }
@@ -916,8 +1110,26 @@ function metricEndpoint(path) {
916
1110
  }
917
1111
  return path;
918
1112
  }
919
- function rateLimitEndpointGroup(path) {
920
- if (path === "/pro/targets/changes") {
1113
+ // The API admits Activity-filtered catalog reads in its heavy "changes" group,
1114
+ // sharing that quota and in-flight limit with the change feeds.
1115
+ function isActivityCatalogRead(path, query = {}) {
1116
+ return (path === "/pro/programs" || path.startsWith("/pro/opportunities/")) && requiresActivityQuery(query);
1117
+ }
1118
+ function activityCircuitBucket(path, query = {}) {
1119
+ const activityCatalog = isActivityCatalogRead(path, query);
1120
+ // Chronological feeds can exceed their own projection budget while complete
1121
+ // rankings remain healthy. Keep this a finite group, never a caller-keyed map.
1122
+ if ((path === "/pro/activity/rankings" && (query.tab === "status" || query.tab === "all_changes"))
1123
+ || path.startsWith("/pro/activity/events/")
1124
+ || (activityCatalog && query.status_change !== undefined && query.status_change !== null)) {
1125
+ return "activity_chronological";
1126
+ }
1127
+ if (/^\/pro\/(?:activity|payouts)\//.test(path))
1128
+ return "activity_reads";
1129
+ return activityCatalog ? "activity_catalog" : undefined;
1130
+ }
1131
+ function rateLimitEndpointGroup(path, query) {
1132
+ if (path === "/pro/targets/changes" || /^\/pro\/(?:history|activity|analytics|payouts)\//.test(path) || isActivityCatalogRead(path, query)) {
921
1133
  return "change_feeds";
922
1134
  }
923
1135
  if (path === "/pro/repositories/changes") {
@@ -940,8 +1152,9 @@ function rateLimitEndpointGroup(path) {
940
1152
  }
941
1153
  return "other";
942
1154
  }
943
- function concurrencyEndpointGroup(path) {
944
- if (path === "/pro/targets/changes" || path === "/pro/repositories/changes") {
1155
+ function concurrencyEndpointGroup(path, query) {
1156
+ if (path === "/pro/targets/changes" || path === "/pro/repositories/changes" || /^\/pro\/(?:history|activity|analytics|payouts)\//.test(path)
1157
+ || isActivityCatalogRead(path, query)) {
945
1158
  return "change_feeds";
946
1159
  }
947
1160
  if (path === "/pro/targets/export") {
@@ -1073,6 +1286,7 @@ function cloneApiError(error, requestId, coalesced = error instanceof BBRadarApi
1073
1286
  cached: error.cached,
1074
1287
  coalesced,
1075
1288
  cacheExpiresAt: error.cacheExpiresAt,
1289
+ cacheAllowed: error.cacheAllowed,
1076
1290
  cause: error
1077
1291
  });
1078
1292
  }