@bbradar/mcp 0.2.3 → 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.
@@ -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;
@@ -13,6 +81,8 @@ const MAX_REQUEST_BODY_BYTES = 64 * 1024;
13
81
  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;
84
+ const SNAPSHOT_START_MIN_TIMEOUT_MS = 60_000;
85
+ const MAX_QUEUED_REQUESTS = 100;
16
86
  export class BBRadarApiError extends Error {
17
87
  requestId;
18
88
  upstreamRequestId;
@@ -22,6 +92,7 @@ export class BBRadarApiError extends Error {
22
92
  cached;
23
93
  coalesced;
24
94
  cacheExpiresAt;
95
+ cacheAllowed;
25
96
  constructor(params) {
26
97
  super(params.message, { cause: params.cause });
27
98
  this.name = "BBRadarApiError";
@@ -33,6 +104,7 @@ export class BBRadarApiError extends Error {
33
104
  this.cached = params.cached ?? false;
34
105
  this.coalesced = params.coalesced ?? false;
35
106
  this.cacheExpiresAt = params.cacheExpiresAt;
107
+ this.cacheAllowed = params.cacheAllowed ?? true;
36
108
  }
37
109
  }
38
110
  export class BBRadarClient {
@@ -40,6 +112,8 @@ export class BBRadarClient {
40
112
  cache;
41
113
  negativeCache;
42
114
  inFlight = new Map();
115
+ sharedRequests = new Map();
116
+ requestContext = new AsyncLocalStorage();
43
117
  endpointMetrics = new Map();
44
118
  requestQueue = [];
45
119
  endpointRequestQueues = new Map();
@@ -56,6 +130,24 @@ export class BBRadarClient {
56
130
  this.cache = new TtlCache(config.cacheMaxEntries, config.cacheTtlMs, config.programStaleWhileRevalidateMs ?? DEFAULT_PROGRAM_SWR_MS);
57
131
  this.negativeCache = new TtlCache(config.cacheMaxEntries, config.negativeCacheTtlMs);
58
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
+ }
59
151
  diagnostics() {
60
152
  const endpointEntries = [...this.endpointMetrics.entries()];
61
153
  const totalRequests = endpointEntries.reduce((total, [, metric]) => total + metric.requests, 0);
@@ -176,6 +268,16 @@ export class BBRadarClient {
176
268
  ? [...value]
177
269
  : [...fallback];
178
270
  }
271
+ supportsStableSnapshot(endpoint) {
272
+ if (this.capabilityState.status !== "negotiated") {
273
+ return false;
274
+ }
275
+ const contract = asRecord(this.capabilityState.contract);
276
+ const behavior = asRecord(contract?.behavior);
277
+ const stableSnapshots = asRecord(behavior?.stable_snapshots);
278
+ const endpoints = stableSnapshots?.endpoints;
279
+ return Array.isArray(endpoints) && endpoints.includes(endpoint);
280
+ }
179
281
  async fetchCapabilityContract() {
180
282
  try {
181
283
  const result = await this.getCapabilities({ cache: false, coalesce: false });
@@ -201,10 +303,11 @@ export class BBRadarClient {
201
303
  throw error;
202
304
  }
203
305
  }
204
- listPrograms(query) {
306
+ listPrograms(query, controls = {}) {
205
307
  return this.request({
206
308
  path: "/pro/programs",
207
- query
309
+ query,
310
+ ...snapshotAwareReadControls(query, controls, true, this.config.requestTimeoutMs)
208
311
  });
209
312
  }
210
313
  getCapabilities(options = {}) {
@@ -229,24 +332,25 @@ export class BBRadarClient {
229
332
  }
230
333
  });
231
334
  }
232
- getRecentChanges(query) {
335
+ getRecentChanges(query, controls = {}) {
233
336
  return this.request({
234
337
  path: "/pro/targets/changes",
235
338
  query,
236
- cache: query.after_id === undefined
339
+ ...snapshotAwareReadControls(query, controls, query.after_id === undefined, this.config.requestTimeoutMs)
237
340
  });
238
341
  }
239
- getRepositoryChanges(query) {
342
+ getRepositoryChanges(query, controls = {}) {
240
343
  return this.request({
241
344
  path: "/pro/repositories/changes",
242
345
  query,
243
- cache: query.after_id === undefined
346
+ ...snapshotAwareReadControls(query, controls, query.after_id === undefined, this.config.requestTimeoutMs)
244
347
  });
245
348
  }
246
- searchTargets(query) {
349
+ searchTargets(query, controls = {}) {
247
350
  return this.request({
248
351
  path: "/pro/targets/search",
249
- query
352
+ query,
353
+ ...snapshotAwareReadControls(query, controls, true, this.config.requestTimeoutMs)
250
354
  });
251
355
  }
252
356
  getOpportunities(level, query) {
@@ -255,6 +359,27 @@ export class BBRadarClient {
255
359
  query
256
360
  });
257
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
+ }
258
383
  exportTargets(body) {
259
384
  return this.request({
260
385
  method: "POST",
@@ -265,8 +390,33 @@ export class BBRadarClient {
265
390
  });
266
391
  }
267
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) {
268
416
  const method = options.method ?? "GET";
269
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 });
270
420
  const url = this.buildUrl(options.path, options.query);
271
421
  const requestKey = stableStringify({
272
422
  method,
@@ -286,7 +436,7 @@ export class BBRadarClient {
286
436
  this.refreshInBackground(options, method, url, requestKey);
287
437
  }
288
438
  return {
289
- ...cached.value,
439
+ ...publicPayload(cached.value),
290
440
  requestId,
291
441
  cached: true,
292
442
  stale: cached.stale,
@@ -307,11 +457,11 @@ export class BBRadarClient {
307
457
  }
308
458
  if (coalesceable) {
309
459
  const pending = this.inFlight.get(requestKey);
310
- if (pending) {
460
+ if (pending && !this.sharedRequests.get(requestKey)?.controller.signal.aborted) {
311
461
  try {
312
- const result = (await pending);
462
+ const result = (await subscribeToRequest(pending, requestId, signal, this.sharedRequests.get(requestKey)));
313
463
  return {
314
- ...result,
464
+ ...publicPayload(result),
315
465
  requestId,
316
466
  cached: false,
317
467
  coalesced: true
@@ -322,27 +472,20 @@ export class BBRadarClient {
322
472
  }
323
473
  }
324
474
  }
325
- const request = this.fetchFromApi(options, method, url, requestId);
326
- if (coalesceable) {
327
- this.inFlight.set(requestKey, request);
328
- }
329
- try {
330
- const result = await request;
331
- 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) {
332
479
  this.cache.set(requestKey, result);
333
480
  }
334
481
  if (negativeCacheable) {
335
482
  this.negativeCache.delete(requestKey);
336
483
  }
337
- return {
338
- ...result,
339
- requestId,
340
- cached: false
341
- };
342
- }
343
- catch (error) {
484
+ return result;
485
+ }).catch((error) => {
344
486
  if (negativeCacheable &&
345
487
  error instanceof BBRadarApiError &&
488
+ error.cacheAllowed &&
346
489
  error.status !== undefined &&
347
490
  isNegativeCacheStatus(error.status)) {
348
491
  this.negativeCache.set(requestKey, {
@@ -354,14 +497,21 @@ export class BBRadarClient {
354
497
  });
355
498
  }
356
499
  throw error;
357
- }
358
- finally {
359
- if (coalesceable) {
500
+ }).finally(() => {
501
+ if (coalesceable && this.inFlight.get(requestKey) === request) {
360
502
  this.inFlight.delete(requestKey);
503
+ this.sharedRequests.delete(requestKey);
361
504
  }
505
+ });
506
+ if (coalesceable) {
507
+ this.inFlight.set(requestKey, request);
508
+ this.sharedRequests.set(requestKey, shared);
362
509
  }
510
+ const result = await subscribeToRequest(request, requestId, signal, shared);
511
+ return { ...publicPayload(result), requestId, cached: false };
363
512
  }
364
513
  async fetchFromApi(options, method, url, requestId) {
514
+ const deadline = Date.now() + (options.timeoutMs ?? this.config.requestTimeoutMs);
365
515
  const serializedBody = options.body === undefined ? undefined : JSON.stringify(options.body);
366
516
  if (serializedBody !== undefined) {
367
517
  const bodyBytes = Buffer.byteLength(serializedBody, "utf8");
@@ -376,11 +526,20 @@ export class BBRadarClient {
376
526
  }
377
527
  return this.withRequestSlot(options.path, async () => {
378
528
  const rateLimitBucket = rateLimitEndpointGroup(options.path);
529
+ if (options.signal?.aborted)
530
+ throw new BBRadarApiError({ message: "BBRadar API request was cancelled.", requestId, status: 408 });
379
531
  this.assertCircuitClosed(requestId, rateLimitBucket);
380
532
  this.reserveUpstreamRequest(requestId, rateLimitBucket);
381
533
  const startedAt = Date.now();
382
534
  const controller = new AbortController();
383
- const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
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();
384
543
  let status;
385
544
  let failed = true;
386
545
  try {
@@ -394,6 +553,7 @@ export class BBRadarClient {
394
553
  }
395
554
  const response = await fetch(url, init);
396
555
  status = response.status;
556
+ const noStore = /(?:^|,)\s*(?:no-store|no-cache|private)\b/i.test(response.headers.get("cache-control") ?? "");
397
557
  this.observeCircuitResponse(rateLimitBucket, response.status);
398
558
  this.observeUpstreamRateLimit(rateLimitBucket, response.headers, response.status);
399
559
  const upstreamRequestId = extractRequestId(response.headers);
@@ -405,7 +565,8 @@ export class BBRadarClient {
405
565
  requestId,
406
566
  status: response.status,
407
567
  detail: extractObjectField(data, "detail"),
408
- errors: extractObjectField(data, "errors")
568
+ errors: extractObjectField(data, "errors"),
569
+ cacheAllowed: !noStore
409
570
  };
410
571
  if (upstreamRequestId) {
411
572
  params.upstreamRequestId = upstreamRequestId;
@@ -415,7 +576,9 @@ export class BBRadarClient {
415
576
  const result = {
416
577
  data: data,
417
578
  status: response.status,
418
- fetchedAt: new Date().toISOString()
579
+ fetchedAt: new Date().toISOString(),
580
+ noStore,
581
+ cacheAllowed: !noStore
419
582
  };
420
583
  if (upstreamRequestId) {
421
584
  result.upstreamRequestId = upstreamRequestId;
@@ -427,6 +590,12 @@ export class BBRadarClient {
427
590
  if (error instanceof BBRadarApiError) {
428
591
  throw error;
429
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
+ }
430
599
  this.observeCircuitFailure(rateLimitBucket);
431
600
  throw new BBRadarApiError({
432
601
  message: error instanceof Error && error.name === "AbortError" ? "BBRadar API request timed out." : "BBRadar API request failed.",
@@ -436,9 +605,10 @@ export class BBRadarClient {
436
605
  }
437
606
  finally {
438
607
  clearTimeout(timeout);
608
+ options.signal?.removeEventListener("abort", onAbort);
439
609
  this.recordEndpointMetric(options.path, Date.now() - startedAt, status, failed);
440
610
  }
441
- });
611
+ }, deadline, requestId, options.signal);
442
612
  }
443
613
  refreshInBackground(options, method, url, requestKey) {
444
614
  if (this.inFlight.has(requestKey)) {
@@ -448,7 +618,12 @@ export class BBRadarClient {
448
618
  this.inFlight.set(requestKey, refresh);
449
619
  void refresh
450
620
  .then((result) => {
451
- this.cache.set(requestKey, result);
621
+ if (result.cacheAllowed !== false) {
622
+ this.cache.set(requestKey, result);
623
+ }
624
+ else {
625
+ this.cache.delete(requestKey);
626
+ }
452
627
  this.negativeCache.delete(requestKey);
453
628
  })
454
629
  .catch(() => undefined)
@@ -458,21 +633,21 @@ export class BBRadarClient {
458
633
  }
459
634
  });
460
635
  }
461
- async withRequestSlot(path, callback) {
636
+ async withRequestSlot(path, callback, deadline, requestId, signal) {
462
637
  const endpointGroup = concurrencyEndpointGroup(path);
463
638
  if (endpointGroup === undefined) {
464
- return this.withGlobalRequestSlot(callback);
639
+ return this.withGlobalRequestSlot(callback, deadline, requestId, signal);
465
640
  }
466
641
  const endpointLimit = endpointConcurrencyLimit(endpointGroup);
467
642
  if (endpointLimit === undefined) {
468
- return this.withGlobalRequestSlot(callback);
643
+ return this.withGlobalRequestSlot(callback, deadline, requestId, signal);
469
644
  }
470
- return this.withEndpointRequestSlot(endpointGroup, endpointLimit, () => this.withGlobalRequestSlot(callback));
645
+ return this.withEndpointRequestSlot(endpointGroup, endpointLimit, () => this.withGlobalRequestSlot(callback, deadline, requestId, signal), deadline, requestId, signal);
471
646
  }
472
- async withGlobalRequestSlot(callback) {
647
+ async withGlobalRequestSlot(callback, deadline, requestId, signal) {
473
648
  const limit = Math.max(1, this.config.maxConcurrentRequests);
474
649
  if (this.activeRequests >= limit) {
475
- await new Promise((resolve) => this.requestQueue.push(resolve));
650
+ await waitForRequestSlot(this.requestQueue, deadline, requestId, signal);
476
651
  }
477
652
  else {
478
653
  this.activeRequests += 1;
@@ -490,14 +665,12 @@ export class BBRadarClient {
490
665
  }
491
666
  }
492
667
  }
493
- async withEndpointRequestSlot(endpointGroup, limit, callback) {
668
+ async withEndpointRequestSlot(endpointGroup, limit, callback, deadline, requestId, signal) {
494
669
  const active = this.endpointActiveRequests.get(endpointGroup) ?? 0;
495
670
  if (active >= limit) {
496
671
  const queue = this.endpointRequestQueues.get(endpointGroup) ?? [];
497
- await new Promise((resolve) => {
498
- queue.push(resolve);
499
- this.endpointRequestQueues.set(endpointGroup, queue);
500
- });
672
+ this.endpointRequestQueues.set(endpointGroup, queue);
673
+ await waitForRequestSlot(queue, deadline, requestId, signal);
501
674
  }
502
675
  else {
503
676
  this.endpointActiveRequests.set(endpointGroup, active + 1);
@@ -787,7 +960,9 @@ export class BBRadarClient {
787
960
  };
788
961
  }
789
962
  buildUrl(path, query) {
790
- const url = new URL(`${this.config.apiBaseUrl}${path}`);
963
+ const base = this.config.apiBaseUrl.replace(/\/+$/, "");
964
+ const prefix = /\/v1$/i.test(base) ? base : `${base}/v1`;
965
+ const url = new URL(`${prefix}${path}`);
791
966
  for (const [key, value] of Object.entries(query ?? {})) {
792
967
  if (value === undefined || value === null) {
793
968
  continue;
@@ -892,6 +1067,16 @@ function toIsoString(timestamp) {
892
1067
  return Number.isNaN(date.getTime()) ? undefined : date.toISOString();
893
1068
  }
894
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";
895
1080
  if (/^\/pro\/programs\/.+\/targets$/.test(path)) {
896
1081
  return "/pro/programs/:program_id/targets";
897
1082
  }
@@ -904,7 +1089,7 @@ function metricEndpoint(path) {
904
1089
  return path;
905
1090
  }
906
1091
  function rateLimitEndpointGroup(path) {
907
- if (path === "/pro/targets/changes") {
1092
+ if (path === "/pro/targets/changes" || /^\/pro\/(?:history|activity|analytics)\//.test(path)) {
908
1093
  return "change_feeds";
909
1094
  }
910
1095
  if (path === "/pro/repositories/changes") {
@@ -928,7 +1113,7 @@ function rateLimitEndpointGroup(path) {
928
1113
  return "other";
929
1114
  }
930
1115
  function concurrencyEndpointGroup(path) {
931
- 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)) {
932
1117
  return "change_feeds";
933
1118
  }
934
1119
  if (path === "/pro/targets/export") {
@@ -945,6 +1130,25 @@ function endpointConcurrencyLimit(endpointGroup) {
945
1130
  }
946
1131
  return undefined;
947
1132
  }
1133
+ function snapshotAwareReadControls(query, controls, defaultCache, defaultTimeoutMs) {
1134
+ const snapshot = typeof query.snapshot === "string" ? query.snapshot : undefined;
1135
+ const result = {
1136
+ // Snapshot responses are explicitly `Cache-Control: no-store`. Keeping
1137
+ // them out of the local TTL cache also prevents an expired token from
1138
+ // being replayed after its five-minute API lifetime.
1139
+ cache: controls.cache ?? (defaultCache && snapshot === undefined),
1140
+ // Two concurrent `snapshot=start` calls must create independent views.
1141
+ // Reusing an already-issued token can still coalesce identical live work.
1142
+ coalesce: controls.coalesce ?? snapshot !== "start"
1143
+ };
1144
+ const timeoutMs = controls.timeoutMs ?? (snapshot === "start"
1145
+ ? Math.max(defaultTimeoutMs, SNAPSHOT_START_MIN_TIMEOUT_MS)
1146
+ : undefined);
1147
+ if (timeoutMs !== undefined) {
1148
+ result.timeoutMs = timeoutMs;
1149
+ }
1150
+ return result;
1151
+ }
948
1152
  function isNegativeCacheStatus(status) {
949
1153
  return status === 404 || status === 405;
950
1154
  }
@@ -1041,6 +1245,7 @@ function cloneApiError(error, requestId, coalesced = error instanceof BBRadarApi
1041
1245
  cached: error.cached,
1042
1246
  coalesced,
1043
1247
  cacheExpiresAt: error.cacheExpiresAt,
1248
+ cacheAllowed: error.cacheAllowed,
1044
1249
  cause: error
1045
1250
  });
1046
1251
  }