@claudinho/core 0.8.19 → 0.9.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.
- package/dist/index.d.ts +59 -4
- package/dist/index.js +133 -34
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -485,6 +485,18 @@ interface ProviderAdapter {
|
|
|
485
485
|
fetchStandings?(): Promise<GroupStandings[]>;
|
|
486
486
|
/** Optional push subscription (websocket/SSE providers). Returns an unsubscribe fn. */
|
|
487
487
|
subscribe?(onBatch: (matches: Match[]) => void): () => void;
|
|
488
|
+
/**
|
|
489
|
+
* Optional: the most recent request failure, cleared when a new request
|
|
490
|
+
* starts (best-effort under concurrency). Lets a caller whose result path
|
|
491
|
+
* fails closed to `degraded` booleans still distinguish a throttle/block
|
|
492
|
+
* (`throttled` — persist a backoff, hammering makes it worse) from an
|
|
493
|
+
* ordinary blip (retry on the normal cadence).
|
|
494
|
+
*/
|
|
495
|
+
readonly lastError?: {
|
|
496
|
+
readonly kind: string;
|
|
497
|
+
readonly status?: number;
|
|
498
|
+
readonly throttled?: boolean;
|
|
499
|
+
};
|
|
488
500
|
}
|
|
489
501
|
|
|
490
502
|
/**
|
|
@@ -503,6 +515,19 @@ interface ProviderAdapter {
|
|
|
503
515
|
declare const DEFAULT_COMPETITION = "fifa.world";
|
|
504
516
|
/** Build an ESPN soccer base URL for a competition slug (e.g. "fifa.friendly"). */
|
|
505
517
|
declare function competitionBase(slug: string): string;
|
|
518
|
+
type ProviderErrorKind = 'http' | 'timeout' | 'parse';
|
|
519
|
+
/**
|
|
520
|
+
* Classified provider failure, so a caller that got a degraded result can tell
|
|
521
|
+
* a throttle/block (back off — hammering makes it worse) from a timeout or a
|
|
522
|
+
* shape change (plain degraded, retry on the normal cadence).
|
|
523
|
+
*/
|
|
524
|
+
declare class ProviderError extends Error {
|
|
525
|
+
readonly kind: ProviderErrorKind;
|
|
526
|
+
readonly status?: number;
|
|
527
|
+
constructor(message: string, kind: ProviderErrorKind, status?: number);
|
|
528
|
+
/** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
|
|
529
|
+
get throttled(): boolean;
|
|
530
|
+
}
|
|
506
531
|
interface EspnStatusType {
|
|
507
532
|
name?: string;
|
|
508
533
|
state?: string;
|
|
@@ -608,6 +633,20 @@ declare class EspnAdapter implements ProviderAdapter {
|
|
|
608
633
|
readonly capabilities: ProviderCapabilities;
|
|
609
634
|
/** Cached team-code -> group-letter map (built lazily from standings). */
|
|
610
635
|
private groupMap?;
|
|
636
|
+
/**
|
|
637
|
+
* One in-flight/recent standings fetch shared by fetchStandings and
|
|
638
|
+
* fetchGroupMap, so a command like `bracket` hits the endpoint once instead
|
|
639
|
+
* of twice, and a long-lived MCP server stays fresh (short TTL). A rejected
|
|
640
|
+
* fetch clears the slot — a transient failure is never cached.
|
|
641
|
+
*/
|
|
642
|
+
private standingsShared?;
|
|
643
|
+
/**
|
|
644
|
+
* The most recent request failure (best-effort under concurrency; cleared
|
|
645
|
+
* when a new request starts). A post-hoc hint for callers whose result path
|
|
646
|
+
* fails closed to `degraded` booleans but who still need to distinguish a
|
|
647
|
+
* throttle (persist a backoff) from an ordinary blip.
|
|
648
|
+
*/
|
|
649
|
+
lastError?: ProviderError;
|
|
611
650
|
constructor(opts?: EspnAdapterOptions);
|
|
612
651
|
fetchByDate(dateISO: string): Promise<Match[]>;
|
|
613
652
|
fetchWindow(startDate: string, endDate: string): Promise<Match[]>;
|
|
@@ -620,6 +659,8 @@ declare class EspnAdapter implements ProviderAdapter {
|
|
|
620
659
|
fetchLive(): Promise<Match[]>;
|
|
621
660
|
/** Standings endpoint URL (lives under apis/v2, not site/v2; derived from base). */
|
|
622
661
|
private standingsUrl;
|
|
662
|
+
/** The shared standings fetch (see {@link standingsShared}). */
|
|
663
|
+
private sharedStandings;
|
|
623
664
|
/**
|
|
624
665
|
* Authoritative, cumulative group tables from the standings endpoint. Throws
|
|
625
666
|
* on fetch/parse failure (the caller decides the fallback). Group-stage only:
|
|
@@ -628,8 +669,11 @@ declare class EspnAdapter implements ProviderAdapter {
|
|
|
628
669
|
fetchStandings(): Promise<GroupStandings[]>;
|
|
629
670
|
/**
|
|
630
671
|
* Build (and cache) a team-code -> group-letter map from the standings
|
|
631
|
-
* endpoint. Best-effort: returns {} if standings are unavailable
|
|
632
|
-
*
|
|
672
|
+
* endpoint. Best-effort: returns {} if standings are unavailable — but a
|
|
673
|
+
* transient failure is NOT cached (only a successful parse pins the map), so
|
|
674
|
+
* one blip can't silently drop group letters for the adapter's lifetime.
|
|
675
|
+
* Reuses the same parse/fetch as {@link fetchStandings}, so the two never
|
|
676
|
+
* drift and one command never fetches standings twice.
|
|
633
677
|
*/
|
|
634
678
|
fetchGroupMap(force?: boolean): Promise<Record<string, string>>;
|
|
635
679
|
private fetchScoreboard;
|
|
@@ -644,7 +688,14 @@ declare class EspnAdapter implements ProviderAdapter {
|
|
|
644
688
|
* always the World Cup.
|
|
645
689
|
*/
|
|
646
690
|
declare function resolveCompetition(explicit?: string): string;
|
|
647
|
-
/**
|
|
691
|
+
/** Provider names {@link makeAdapter} can construct (the CLI validates against this). */
|
|
692
|
+
declare const KNOWN_SOURCES: readonly ["espn"];
|
|
693
|
+
/**
|
|
694
|
+
* Construct a provider adapter for a `--source` name (default: espn). An
|
|
695
|
+
* unknown source FAILS LOUD instead of silently running ESPN — `--source foo`
|
|
696
|
+
* previously no-op'd, which lied about what the flag did (attribution stayed
|
|
697
|
+
* honest, but the advertised knob did nothing).
|
|
698
|
+
*/
|
|
648
699
|
declare function makeAdapter(source?: string): ProviderAdapter;
|
|
649
700
|
/**
|
|
650
701
|
* Merge live matches over a base set by id. Live entries replace base entries
|
|
@@ -690,6 +741,10 @@ interface StandingsResult {
|
|
|
690
741
|
* asked-for group isn't in it (caller renders "no such group").
|
|
691
742
|
*/
|
|
692
743
|
declare function getStandings(adapter: ProviderAdapter, group?: string): Promise<StandingsResult>;
|
|
744
|
+
declare function knockoutWindow(): {
|
|
745
|
+
start: string;
|
|
746
|
+
end: string;
|
|
747
|
+
} | null;
|
|
693
748
|
/**
|
|
694
749
|
* Knockout bracket with hybrid slot resolution: confirmed FT winners/losers from
|
|
695
750
|
* the live overlay; group slots project from live standings once a group has started.
|
|
@@ -1224,4 +1279,4 @@ declare function formatShareBracket(input: ShareBracketInput, options?: ShareBra
|
|
|
1224
1279
|
/** Compact one-line-per-match bracket for narrow share contexts. */
|
|
1225
1280
|
declare function formatBracketCompactLine(mv: BracketMatchView, opts?: BracketFormatOpts): string;
|
|
1226
1281
|
|
|
1227
|
-
export { BRACKET_STAGE_ORDER, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketTopology, type BracketView, type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FEED_TEXT_MAX, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type NextFixtureResult, type Outcome, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, type ShareBracketInput, type ShareBracketOptions, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type Stage, type StandingRow, type StandingsResult, type Status, type Team, type TeamInfo, type TeamLookup, allFixtures, allTeams, asFlavorLevel, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, deriveFavorite, displayWidth, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getStandings, groups, hasSaneDistribution, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isValidDate, isValidTimeZone, liveSourceLabel, loadBracketTopology, localDate, lookupTeam, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseStandings, parseTeamSlot, resolveCompetition, resolveMarketSource, resolveTz, sanitizeBundledFixture, sanitizeFeedText, sanitizeMatchStrings, scoreline, stageLabel, stageLabelI18n, t };
|
|
1282
|
+
export { BRACKET_STAGE_ORDER, type BracketFormatOpts, type BracketMatchNode, type BracketMatchView, type BracketResult, type BracketTopology, type BracketView, type BuildSignalInput, DEFAULT_COMPETITION, DEFAULT_FLAVOR, DEFAULT_MAX_AGE_MS, EspnAdapter, type EspnAdapterOptions, FEED_TEXT_MAX, FLAVOR_LEVELS, FakeMarketProvider, type FakeMarketProviderOptions, type FavoriteStrength, type FlavorLevel, type FormatOpts, type GroupStandings, KNOWN_SOURCES, type KnockoutFixturesResult, LIVE_WINDOW_MS, type Lang, type LedgerRow, type LiveResult, type MapContext, type MarketFavorite, type MarketMapping, type MarketMappingTable, type MarketOutcome, type MarketOutcomeKind, type MarketProvider, type MarketSignal, type MarketSignalOptions, type MarketSignalsResult, type Match, type MatchByIdResult, type MatchEvent, type NextFixtureResult, type Outcome, PolymarketProvider, type PolymarketProviderOptions, type ProviderAdapter, type ProviderCapabilities, ProviderError, type ProviderErrorKind, type PunditPick, type ResolvedParticipant, SHARE_DISCLAIMER, SHARE_HASHTAG, type ShareBracketInput, type ShareBracketOptions, type ShareSnippetInput, type ShareSnippetOptions, type ShareStyle, type ShareTableInput, type SlotRef, type Stage, type StandingRow, type StandingsResult, type Status, type Team, type TeamInfo, type TeamLookup, allFixtures, allTeams, asFlavorLevel, buildBracketTopology, buildBracketView, buildMarketSignal, byKickoff, competitionBase, computeStandings, countdown, currentOrNextFixtureForTeam, deriveFavorite, displayWidth, favoriteStrength, fixturesByDate, fixturesByGroup, fixturesByTeam, fixturesInLiveWindow, flagEmoji, formatBracketCompactLine, formatBracketList, formatBracketMatchLine, formatBracketTree, formatDate, formatKickoff, formatShareBracket, formatShareSnippet, formatShareTable, formatTime, getBracket, getKnockoutFixtures, getLiveMatches, getMarketSignal, getMarketSignals, getMatchById, getMatchesForDate, getNextFixtureForTeam, getStandings, groups, hasSaneDistribution, isFinished, isFlavorLevel, isLive, isReliableMarketSignal, isResolvedNation, isStaleSignal, isValidDate, isValidTimeZone, knockoutWindow, liveSourceLabel, loadBracketTopology, localDate, lookupTeam, makeAdapter, makeMarketProvider, mapEspnEvent, mapsCleanly, marketAttributionText, marketBlock, marketFavoriteText, marketFixtureForTeam, marketLine, marketProbabilityText, marketRelevant, marketSignalRendersFor, marketSourceLabel, matchFlavor, matchKey, matchLocation, mergeLive, nationToFlag, nationToRegion, nextFixtureForTeam, normalizeLang, normalizeOutcomes, outcomeFromScore, padVisible, parseStandings, parseTeamSlot, resolveCompetition, resolveMarketSource, resolveTz, sanitizeBundledFixture, sanitizeFeedText, sanitizeMatchStrings, scoreline, stageLabel, stageLabelI18n, t };
|
package/dist/index.js
CHANGED
|
@@ -378,7 +378,8 @@ function normalizeLang(lang) {
|
|
|
378
378
|
}
|
|
379
379
|
function t(lang, key, vars) {
|
|
380
380
|
const dict = CATALOGS[normalizeLang(lang)];
|
|
381
|
-
|
|
381
|
+
const en = EN;
|
|
382
|
+
let s = dict[key] ?? en[key] ?? key;
|
|
382
383
|
if (vars) {
|
|
383
384
|
for (const [k, v] of Object.entries(vars)) s = s.replaceAll(`{${k}}`, v);
|
|
384
385
|
}
|
|
@@ -3085,11 +3086,27 @@ function rosterAtZero(matches) {
|
|
|
3085
3086
|
var ESPN_SOCCER = "https://site.api.espn.com/apis/site/v2/sports/soccer";
|
|
3086
3087
|
var DEFAULT_COMPETITION = "fifa.world";
|
|
3087
3088
|
var DEFAULT_BASE = `${ESPN_SOCCER}/${DEFAULT_COMPETITION}`;
|
|
3088
|
-
var USER_AGENT = "
|
|
3089
|
+
var USER_AGENT = `claudinho/${"0.9.0"} (+https://github.com/arturogarrido/claudinho)`;
|
|
3089
3090
|
var MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
3090
3091
|
function competitionBase(slug) {
|
|
3091
3092
|
return `${ESPN_SOCCER}/${slug}`;
|
|
3092
3093
|
}
|
|
3094
|
+
var DEFAULT_TIMEOUT_MS = 6e3;
|
|
3095
|
+
var STANDINGS_SHARE_MS = 3e4;
|
|
3096
|
+
var ProviderError = class extends Error {
|
|
3097
|
+
kind;
|
|
3098
|
+
status;
|
|
3099
|
+
constructor(message, kind, status) {
|
|
3100
|
+
super(message);
|
|
3101
|
+
this.name = "ProviderError";
|
|
3102
|
+
this.kind = kind;
|
|
3103
|
+
this.status = status;
|
|
3104
|
+
}
|
|
3105
|
+
/** 429/403 — the upstream is refusing us; retrying at the live cadence makes it worse. */
|
|
3106
|
+
get throttled() {
|
|
3107
|
+
return this.kind === "http" && (this.status === 429 || this.status === 403);
|
|
3108
|
+
}
|
|
3109
|
+
};
|
|
3093
3110
|
function mapStatus(st) {
|
|
3094
3111
|
const name = (st?.type?.name ?? "").toUpperCase();
|
|
3095
3112
|
const state = st?.type?.state ?? "";
|
|
@@ -3231,6 +3248,20 @@ var EspnAdapter = class {
|
|
|
3231
3248
|
capabilities = { push: false, latencyHintSec: 45 };
|
|
3232
3249
|
/** Cached team-code -> group-letter map (built lazily from standings). */
|
|
3233
3250
|
groupMap;
|
|
3251
|
+
/**
|
|
3252
|
+
* One in-flight/recent standings fetch shared by fetchStandings and
|
|
3253
|
+
* fetchGroupMap, so a command like `bracket` hits the endpoint once instead
|
|
3254
|
+
* of twice, and a long-lived MCP server stays fresh (short TTL). A rejected
|
|
3255
|
+
* fetch clears the slot — a transient failure is never cached.
|
|
3256
|
+
*/
|
|
3257
|
+
standingsShared;
|
|
3258
|
+
/**
|
|
3259
|
+
* The most recent request failure (best-effort under concurrency; cleared
|
|
3260
|
+
* when a new request starts). A post-hoc hint for callers whose result path
|
|
3261
|
+
* fails closed to `degraded` booleans but who still need to distinguish a
|
|
3262
|
+
* throttle (persist a backoff) from an ordinary blip.
|
|
3263
|
+
*/
|
|
3264
|
+
lastError;
|
|
3234
3265
|
async fetchByDate(dateISO) {
|
|
3235
3266
|
return this.fetchScoreboard(toEspnDate(dateISO));
|
|
3236
3267
|
}
|
|
@@ -3252,37 +3283,58 @@ var EspnAdapter = class {
|
|
|
3252
3283
|
const base = this.opts.baseUrl ?? DEFAULT_BASE;
|
|
3253
3284
|
return `${base.replace("/apis/site/v2/", "/apis/v2/")}/standings`;
|
|
3254
3285
|
}
|
|
3286
|
+
/** The shared standings fetch (see {@link standingsShared}). */
|
|
3287
|
+
sharedStandings() {
|
|
3288
|
+
const now = Date.now();
|
|
3289
|
+
if (this.standingsShared && now - this.standingsShared.at < STANDINGS_SHARE_MS) {
|
|
3290
|
+
return this.standingsShared.promise;
|
|
3291
|
+
}
|
|
3292
|
+
const promise = this.get(this.standingsUrl()).then(
|
|
3293
|
+
(d) => parseStandings(d)
|
|
3294
|
+
);
|
|
3295
|
+
this.standingsShared = { at: now, promise };
|
|
3296
|
+
promise.catch(() => {
|
|
3297
|
+
if (this.standingsShared?.promise === promise) this.standingsShared = void 0;
|
|
3298
|
+
});
|
|
3299
|
+
return promise;
|
|
3300
|
+
}
|
|
3255
3301
|
/**
|
|
3256
3302
|
* Authoritative, cumulative group tables from the standings endpoint. Throws
|
|
3257
3303
|
* on fetch/parse failure (the caller decides the fallback). Group-stage only:
|
|
3258
3304
|
* non-group `children` are filtered out by {@link parseStandings}.
|
|
3259
3305
|
*/
|
|
3260
3306
|
async fetchStandings() {
|
|
3261
|
-
return
|
|
3307
|
+
return this.sharedStandings();
|
|
3262
3308
|
}
|
|
3263
3309
|
/**
|
|
3264
3310
|
* Build (and cache) a team-code -> group-letter map from the standings
|
|
3265
|
-
* endpoint. Best-effort: returns {} if standings are unavailable
|
|
3266
|
-
*
|
|
3311
|
+
* endpoint. Best-effort: returns {} if standings are unavailable — but a
|
|
3312
|
+
* transient failure is NOT cached (only a successful parse pins the map), so
|
|
3313
|
+
* one blip can't silently drop group letters for the adapter's lifetime.
|
|
3314
|
+
* Reuses the same parse/fetch as {@link fetchStandings}, so the two never
|
|
3315
|
+
* drift and one command never fetches standings twice.
|
|
3267
3316
|
*/
|
|
3268
3317
|
async fetchGroupMap(force = false) {
|
|
3269
3318
|
if (this.groupMap && !force) return this.groupMap;
|
|
3270
|
-
const map = {};
|
|
3271
3319
|
try {
|
|
3272
|
-
const tables =
|
|
3320
|
+
const tables = await this.sharedStandings();
|
|
3321
|
+
const map = {};
|
|
3273
3322
|
for (const t2 of tables) for (const r of t2.rows) map[r.team.code] = t2.group;
|
|
3323
|
+
this.groupMap = map;
|
|
3324
|
+
return map;
|
|
3274
3325
|
} catch {
|
|
3326
|
+
return {};
|
|
3275
3327
|
}
|
|
3276
|
-
this.groupMap = map;
|
|
3277
|
-
return map;
|
|
3278
3328
|
}
|
|
3279
3329
|
async fetchScoreboard(dates) {
|
|
3280
3330
|
const base = this.opts.baseUrl ?? DEFAULT_BASE;
|
|
3281
3331
|
const url = new URL(`${base}/scoreboard`);
|
|
3282
3332
|
url.searchParams.set("limit", "300");
|
|
3283
3333
|
if (dates) url.searchParams.set("dates", dates);
|
|
3284
|
-
const groupByTeam =
|
|
3285
|
-
|
|
3334
|
+
const [groupByTeam, data] = await Promise.all([
|
|
3335
|
+
this.opts.enrichGroups === false ? Promise.resolve({}) : this.fetchGroupMap(),
|
|
3336
|
+
this.get(url.toString())
|
|
3337
|
+
]);
|
|
3286
3338
|
return (data.events ?? []).map((ev) => mapEspnEvent(ev, { groupByTeam }));
|
|
3287
3339
|
}
|
|
3288
3340
|
async get(url) {
|
|
@@ -3290,24 +3342,51 @@ var EspnAdapter = class {
|
|
|
3290
3342
|
const controller = new AbortController();
|
|
3291
3343
|
const timer = setTimeout(
|
|
3292
3344
|
() => controller.abort(),
|
|
3293
|
-
this.opts.timeoutMs ??
|
|
3345
|
+
this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
3294
3346
|
);
|
|
3347
|
+
this.lastError = void 0;
|
|
3295
3348
|
try {
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3349
|
+
let res;
|
|
3350
|
+
try {
|
|
3351
|
+
res = await doFetch(url, {
|
|
3352
|
+
signal: controller.signal,
|
|
3353
|
+
// ESPN never legitimately redirects; following one would sidestep the
|
|
3354
|
+
// fixed-host guarantee, so treat any redirect as a failure (degraded).
|
|
3355
|
+
redirect: "error",
|
|
3356
|
+
headers: { Accept: "application/json", "User-Agent": USER_AGENT }
|
|
3357
|
+
});
|
|
3358
|
+
} catch (e) {
|
|
3359
|
+
throw e?.name === "AbortError" ? new ProviderError(`ESPN request timed out: ${url}`, "timeout") : new ProviderError(`ESPN request failed: ${e?.message ?? e}`, "http");
|
|
3360
|
+
}
|
|
3303
3361
|
if (!res.ok) {
|
|
3304
|
-
throw new
|
|
3362
|
+
throw new ProviderError(
|
|
3363
|
+
`ESPN request failed: ${res.status} ${res.statusText}`,
|
|
3364
|
+
"http",
|
|
3365
|
+
res.status
|
|
3366
|
+
);
|
|
3305
3367
|
}
|
|
3306
3368
|
const length = Number(res.headers?.get?.("content-length"));
|
|
3307
3369
|
if (Number.isFinite(length) && length > MAX_RESPONSE_BYTES) {
|
|
3308
|
-
throw new
|
|
3370
|
+
throw new ProviderError(`ESPN response too large: ${length} bytes`, "parse");
|
|
3371
|
+
}
|
|
3372
|
+
try {
|
|
3373
|
+
return await res.json();
|
|
3374
|
+
} catch (e) {
|
|
3375
|
+
throw new ProviderError(
|
|
3376
|
+
`ESPN response unparseable: ${e?.message ?? e}`,
|
|
3377
|
+
"parse"
|
|
3378
|
+
);
|
|
3309
3379
|
}
|
|
3310
|
-
|
|
3380
|
+
} catch (e) {
|
|
3381
|
+
const pe = e instanceof ProviderError ? e : new ProviderError(String(e?.message ?? e), "http");
|
|
3382
|
+
this.lastError = pe;
|
|
3383
|
+
if (typeof process !== "undefined" && process.env?.CLAUDINHO_DEBUG) {
|
|
3384
|
+
process.stderr.write(
|
|
3385
|
+
`claudinho: espn ${pe.kind}${pe.status ? ` ${pe.status}` : ""}: ${url}
|
|
3386
|
+
`
|
|
3387
|
+
);
|
|
3388
|
+
}
|
|
3389
|
+
throw pe;
|
|
3311
3390
|
} finally {
|
|
3312
3391
|
clearTimeout(timer);
|
|
3313
3392
|
}
|
|
@@ -4201,13 +4280,18 @@ function resolveCompetition(explicit) {
|
|
|
4201
4280
|
}
|
|
4202
4281
|
return DEFAULT_COMPETITION;
|
|
4203
4282
|
}
|
|
4283
|
+
var KNOWN_SOURCES = ["espn"];
|
|
4204
4284
|
function makeAdapter(source = "espn") {
|
|
4205
4285
|
switch (source) {
|
|
4206
|
-
|
|
4286
|
+
case "espn": {
|
|
4207
4287
|
const competition = resolveCompetition();
|
|
4208
4288
|
const baseUrl = competition === DEFAULT_COMPETITION ? void 0 : competitionBase(competition);
|
|
4209
4289
|
return new EspnAdapter({ baseUrl });
|
|
4210
4290
|
}
|
|
4291
|
+
default:
|
|
4292
|
+
throw new Error(
|
|
4293
|
+
`Unknown data source "${source}" (available: ${KNOWN_SOURCES.join(", ")})`
|
|
4294
|
+
);
|
|
4211
4295
|
}
|
|
4212
4296
|
}
|
|
4213
4297
|
function mergeLive(base, live) {
|
|
@@ -4245,8 +4329,16 @@ async function getStandings(adapter, group) {
|
|
|
4245
4329
|
const tables = letters.map((g) => ({ group: g, rows: rosterAtZero(fixturesByGroup(g)) })).filter((t2) => t2.rows.length > 0);
|
|
4246
4330
|
return { tables, degraded: true };
|
|
4247
4331
|
}
|
|
4248
|
-
var
|
|
4249
|
-
|
|
4332
|
+
var knockoutWindowMemo;
|
|
4333
|
+
function knockoutWindow() {
|
|
4334
|
+
if (knockoutWindowMemo !== void 0) return knockoutWindowMemo;
|
|
4335
|
+
const days = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY").map((m) => m.kickoff.slice(0, 10).replace(/-/g, "")).filter((d) => d.length === 8);
|
|
4336
|
+
knockoutWindowMemo = days.length ? {
|
|
4337
|
+
start: days.reduce((a, b) => a < b ? a : b),
|
|
4338
|
+
end: days.reduce((a, b) => a > b ? a : b)
|
|
4339
|
+
} : null;
|
|
4340
|
+
return knockoutWindowMemo;
|
|
4341
|
+
}
|
|
4250
4342
|
async function getBracket(adapter, opts = {}) {
|
|
4251
4343
|
const topology = loadBracketTopology();
|
|
4252
4344
|
const base = allFixtures().filter((m) => m.stage !== "GROUP" && m.stage !== "FRIENDLY");
|
|
@@ -4254,7 +4346,8 @@ async function getBracket(adapter, opts = {}) {
|
|
|
4254
4346
|
let liveDegraded = true;
|
|
4255
4347
|
let source;
|
|
4256
4348
|
try {
|
|
4257
|
-
const
|
|
4349
|
+
const win = knockoutWindow();
|
|
4350
|
+
const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
|
|
4258
4351
|
matches = mergeLive(base, live);
|
|
4259
4352
|
liveDegraded = false;
|
|
4260
4353
|
source = adapter.name;
|
|
@@ -4287,8 +4380,9 @@ async function marketFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Dat
|
|
|
4287
4380
|
let fixtures = allFixtures();
|
|
4288
4381
|
let overlayFailed = false;
|
|
4289
4382
|
try {
|
|
4290
|
-
|
|
4291
|
-
|
|
4383
|
+
const win = knockoutWindow();
|
|
4384
|
+
if (adapter.fetchWindow && win) {
|
|
4385
|
+
fixtures = mergeLive(fixtures, await adapter.fetchWindow(win.start, win.end));
|
|
4292
4386
|
}
|
|
4293
4387
|
} catch {
|
|
4294
4388
|
overlayFailed = true;
|
|
@@ -4311,7 +4405,8 @@ async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Da
|
|
|
4311
4405
|
let degraded = true;
|
|
4312
4406
|
let liveById;
|
|
4313
4407
|
try {
|
|
4314
|
-
const
|
|
4408
|
+
const win = knockoutWindow();
|
|
4409
|
+
const live = adapter.fetchWindow && win ? await adapter.fetchWindow(win.start, win.end) : [];
|
|
4315
4410
|
matches = mergeLive(base, live);
|
|
4316
4411
|
degraded = false;
|
|
4317
4412
|
liveById = new Set(live.map((m) => m.id));
|
|
@@ -4322,10 +4417,11 @@ async function getNextFixtureForTeam(adapter, code, now = /* @__PURE__ */ new Da
|
|
|
4322
4417
|
return { fixture, degraded, source };
|
|
4323
4418
|
}
|
|
4324
4419
|
async function getKnockoutFixtures(adapter, now = /* @__PURE__ */ new Date()) {
|
|
4325
|
-
|
|
4420
|
+
const win = knockoutWindow();
|
|
4421
|
+
if (!adapter.fetchWindow || !win) return { fixtures: [], degraded: true };
|
|
4326
4422
|
let live;
|
|
4327
4423
|
try {
|
|
4328
|
-
live = await adapter.fetchWindow(
|
|
4424
|
+
live = await adapter.fetchWindow(win.start, win.end);
|
|
4329
4425
|
} catch {
|
|
4330
4426
|
return { fixtures: [], degraded: true };
|
|
4331
4427
|
}
|
|
@@ -4583,7 +4679,7 @@ var mapping_2026_default = {
|
|
|
4583
4679
|
var DEFAULT_BASE2 = "https://gamma-api.polymarket.com";
|
|
4584
4680
|
var ALLOWED_HOSTS = /* @__PURE__ */ new Set(["gamma-api.polymarket.com"]);
|
|
4585
4681
|
var USER_AGENT2 = "claudinho/0.0 (+https://github.com/arturogarrido/claudinho)";
|
|
4586
|
-
var
|
|
4682
|
+
var DEFAULT_TIMEOUT_MS2 = 8e3;
|
|
4587
4683
|
var WC_SERIES_SLUG = "soccer-fifwc";
|
|
4588
4684
|
var WC_SPORT = "fifwc";
|
|
4589
4685
|
var KICKOFF_TOLERANCE_MS = 6 * 60 * 6e4;
|
|
@@ -4621,7 +4717,7 @@ var PolymarketProvider = class {
|
|
|
4621
4717
|
const entry = (this.opts.mapping ?? BUNDLED_MAPPING)[match.id];
|
|
4622
4718
|
const slugs = entry?.eventSlug ? [entry.eventSlug] : deriveEventSlugs(match);
|
|
4623
4719
|
if (slugs.length === 0) return { checked: true };
|
|
4624
|
-
const configured = options?.timeoutMs ?? this.opts.timeoutMs ??
|
|
4720
|
+
const configured = options?.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
|
|
4625
4721
|
try {
|
|
4626
4722
|
for (const slug of slugs) {
|
|
4627
4723
|
const remaining = deadline - Date.now();
|
|
@@ -4641,7 +4737,7 @@ var PolymarketProvider = class {
|
|
|
4641
4737
|
const url = `${base}/events?slug=${encodeURIComponent(slug)}`;
|
|
4642
4738
|
const doFetch = this.opts.fetchImpl ?? fetch;
|
|
4643
4739
|
const res = await doFetch(url, {
|
|
4644
|
-
signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ??
|
|
4740
|
+
signal: AbortSignal.timeout(timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2),
|
|
4645
4741
|
// Gamma never legitimately redirects; following one would sidestep the
|
|
4646
4742
|
// host allow-list (it only validates the base URL), so reject redirects.
|
|
4647
4743
|
redirect: "error",
|
|
@@ -5110,8 +5206,10 @@ export {
|
|
|
5110
5206
|
FEED_TEXT_MAX,
|
|
5111
5207
|
FLAVOR_LEVELS,
|
|
5112
5208
|
FakeMarketProvider,
|
|
5209
|
+
KNOWN_SOURCES,
|
|
5113
5210
|
LIVE_WINDOW_MS,
|
|
5114
5211
|
PolymarketProvider,
|
|
5212
|
+
ProviderError,
|
|
5115
5213
|
SHARE_DISCLAIMER,
|
|
5116
5214
|
SHARE_HASHTAG,
|
|
5117
5215
|
allFixtures,
|
|
@@ -5162,6 +5260,7 @@ export {
|
|
|
5162
5260
|
isStaleSignal,
|
|
5163
5261
|
isValidDate,
|
|
5164
5262
|
isValidTimeZone,
|
|
5263
|
+
knockoutWindow,
|
|
5165
5264
|
liveSourceLabel,
|
|
5166
5265
|
loadBracketTopology,
|
|
5167
5266
|
localDate,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudinho/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Domain model, provider adapters (ESPN), standings, bundled schedule, and the read-only Polymarket market-signal sidecar powering Claudinho. Not affiliated with FIFA or Anthropic.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
],
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "^22.20.0",
|
|
57
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
57
58
|
"tsup": "^8.0.0",
|
|
58
59
|
"tsx": "^4.19.0",
|
|
59
60
|
"typescript": "^5.7.0",
|