fansunited-data-layer 0.16.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +41 -0
  2. package/api/fansunited/index.d.ts +2 -2
  3. package/api/fansunited/index.d.ts.map +1 -1
  4. package/api/fansunited/search/__tests__/locale-agnostic-cache.test.d.ts +2 -0
  5. package/api/fansunited/search/__tests__/locale-agnostic-cache.test.d.ts.map +1 -0
  6. package/api/fansunited/search/index.d.ts.map +1 -1
  7. package/api/fansunited/search/index.js +41 -25
  8. package/api/fansunited/search/index.js.map +1 -1
  9. package/api/fansunited/search/transformer.d.ts +6 -0
  10. package/api/fansunited/search/transformer.d.ts.map +1 -1
  11. package/api/fansunited/search/transformer.js +4 -2
  12. package/api/fansunited/search/transformer.js.map +1 -1
  13. package/api/fansunited/sports/competition/__tests__/exports.test.d.ts +2 -0
  14. package/api/fansunited/sports/competition/__tests__/exports.test.d.ts.map +1 -0
  15. package/api/fansunited/sports/competition/__tests__/hydrated.test.d.ts +2 -0
  16. package/api/fansunited/sports/competition/__tests__/hydrated.test.d.ts.map +1 -0
  17. package/api/fansunited/sports/competition/__tests__/index.test.d.ts +2 -0
  18. package/api/fansunited/sports/competition/__tests__/index.test.d.ts.map +1 -0
  19. package/api/fansunited/sports/competition/hydrated.d.ts +45 -0
  20. package/api/fansunited/sports/competition/hydrated.d.ts.map +1 -0
  21. package/api/fansunited/sports/competition/hydrated.js +26 -0
  22. package/api/fansunited/sports/competition/hydrated.js.map +1 -0
  23. package/api/fansunited/sports/competition/index.d.ts +3 -12
  24. package/api/fansunited/sports/competition/index.d.ts.map +1 -1
  25. package/api/fansunited/sports/competition/index.js +16 -10
  26. package/api/fansunited/sports/competition/index.js.map +1 -1
  27. package/api/fansunited/sports/index.d.ts +6 -0
  28. package/api/fansunited/sports/index.d.ts.map +1 -1
  29. package/api/fansunited/sports/livescore/__tests__/hydrated.test.d.ts +2 -0
  30. package/api/fansunited/sports/livescore/__tests__/hydrated.test.d.ts.map +1 -0
  31. package/api/fansunited/sports/livescore/hydrated.d.ts +48 -0
  32. package/api/fansunited/sports/livescore/hydrated.d.ts.map +1 -0
  33. package/api/fansunited/sports/livescore/hydrated.js +34 -0
  34. package/api/fansunited/sports/livescore/hydrated.js.map +1 -0
  35. package/api/fansunited/sports/livescore/index.d.ts +27 -0
  36. package/api/fansunited/sports/livescore/index.d.ts.map +1 -0
  37. package/api/fansunited/sports/livescore/index.js +36 -0
  38. package/api/fansunited/sports/livescore/index.js.map +1 -0
  39. package/api/fansunited/sports/livescore/transformer.d.ts +8 -0
  40. package/api/fansunited/sports/livescore/transformer.d.ts.map +1 -0
  41. package/api/fansunited/sports/livescore/transformer.js +44 -0
  42. package/api/fansunited/sports/livescore/transformer.js.map +1 -0
  43. package/api/fansunited/sports/livescore/types.d.ts +58 -0
  44. package/api/fansunited/sports/livescore/types.d.ts.map +1 -0
  45. package/cache/__tests__/redis-integration.test.d.ts +14 -0
  46. package/cache/__tests__/redis-integration.test.d.ts.map +1 -0
  47. package/cache/__tests__/redis-l2-store.d.ts +52 -0
  48. package/cache/__tests__/redis-l2-store.d.ts.map +1 -0
  49. package/cache/__tests__/test-l2-store.d.ts +18 -0
  50. package/cache/__tests__/test-l2-store.d.ts.map +1 -0
  51. package/cache/cache-manager.d.ts +117 -12
  52. package/cache/cache-manager.d.ts.map +1 -1
  53. package/cache/cache-manager.js +23 -14
  54. package/cache/cache-manager.js.map +1 -1
  55. package/cache/cleanup.d.ts +1 -1
  56. package/cache/cleanup.d.ts.map +1 -1
  57. package/cache/index.d.ts +3 -2
  58. package/cache/index.d.ts.map +1 -1
  59. package/cache/sqlite-store.d.ts +4 -4
  60. package/cache/sqlite-store.d.ts.map +1 -1
  61. package/cache/types.d.ts +9 -1
  62. package/cache/types.d.ts.map +1 -1
  63. package/cache/types.js +5 -0
  64. package/cache/types.js.map +1 -0
  65. package/fansunited-data-layer.js +11 -1
  66. package/fansunited-data-layer.js.map +1 -1
  67. package/index.d.ts +5 -5
  68. package/index.d.ts.map +1 -1
  69. package/package.json +2 -1
  70. package/types/canonical/index.d.ts +1 -0
  71. package/types/canonical/index.d.ts.map +1 -1
  72. package/types/canonical/sports-livescore.types.d.ts +84 -0
  73. package/types/canonical/sports-livescore.types.d.ts.map +1 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exports.test.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/api/fansunited/sports/competition/__tests__/exports.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hydrated.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrated.test.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/api/fansunited/sports/competition/__tests__/hydrated.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/api/fansunited/sports/competition/__tests__/index.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Hydrated wrapper for the Fans United Sports API competition endpoint.
3
+ *
4
+ * Composes the (cached) raw competition response with a (cached) batch
5
+ * resolution of every entity ID in `meta.entityIds` via the Search API,
6
+ * and returns one object the consumer can render directly.
7
+ *
8
+ * Cache topology:
9
+ * - sports/competition response → cached under "sportsCompetition"
10
+ * (5min stale / 1h max) by id+season.
11
+ * - search/entity responses → cached under "search" (24h stale / 7d max)
12
+ * by id only; the raw multi-locale payload serves every locale via
13
+ * transformEntity(raw, lang).
14
+ *
15
+ * @example
16
+ * const data = await getFansUnitedSportsCompetitionHydrated("fb:c:1", { locale: "BG" });
17
+ * for (const entry of data.season.stages[0].groups[0].standings) {
18
+ * const team = data.entities.get(entry.competitorId);
19
+ * console.log(`${entry.rank} ${team?.name}`);
20
+ * }
21
+ */
22
+ import type { FUSportsCompetitionDetails } from "../../../../types/canonical/sports-competition.types";
23
+ import type { DataLayerConfig } from "../../../../config";
24
+ import type { NextCacheOptions } from "../../http";
25
+ import type { SearchEntityResult } from "../../search/types";
26
+ export interface GetSportsCompetitionHydratedOptions {
27
+ /** Season identifier (e.g. "fb:c:1:2025/26"). Defaults to the active season. */
28
+ seasonId?: string;
29
+ /** Locale code (e.g. "EN", "BG") used to resolve entity translations. Falls back to EN. */
30
+ locale?: string;
31
+ /** Next.js cache options for ISR/on-demand revalidation. */
32
+ next?: NextCacheOptions;
33
+ }
34
+ /**
35
+ * Sports API competition response plus a lookup map for every referenced entity.
36
+ *
37
+ * The map is keyed by entity ID. Missing IDs (404s from the Search API) are
38
+ * silently omitted — render code should fall back gracefully on a missing entry:
39
+ * `entities.get(id) ?? { name: id }`.
40
+ */
41
+ export interface FUSportsCompetitionHydrated extends FUSportsCompetitionDetails {
42
+ entities: Map<string, SearchEntityResult>;
43
+ }
44
+ export declare function getFansUnitedSportsCompetitionHydrated(competitionId: string, options?: GetSportsCompetitionHydratedOptions, config?: DataLayerConfig): Promise<FUSportsCompetitionHydrated>;
45
+ //# sourceMappingURL=hydrated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrated.d.ts","sourceRoot":"","sources":["../../../../../src/lib/api/fansunited/sports/competition/hydrated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sDAAsD,CAAC;AACvG,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAI7D,MAAM,WAAW,mCAAmC;IAChD,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,2BAA4B,SAAQ,0BAA0B;IAC3E,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC7C;AAED,wBAAsB,sCAAsC,CACxD,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,mCAAmC,EAC7C,MAAM,CAAC,EAAE,eAAe,GACzB,OAAO,CAAC,2BAA2B,CAAC,CAsBtC"}
@@ -0,0 +1,26 @@
1
+ import { getFansUnitedSportsCompetition } from "./index.js";
2
+ import { getFansUnitedEntitiesByIds } from "../../search/index.js";
3
+ async function getFansUnitedSportsCompetitionHydrated(competitionId, options, config) {
4
+ const raw = await getFansUnitedSportsCompetition(
5
+ competitionId,
6
+ { seasonId: options?.seasonId, next: options?.next },
7
+ config
8
+ );
9
+ const entityIds = raw.meta.entityIds;
10
+ const entities = /* @__PURE__ */ new Map();
11
+ if (entityIds.length > 0) {
12
+ const resolved = await getFansUnitedEntitiesByIds(entityIds, {
13
+ lang: options?.locale,
14
+ next: options?.next,
15
+ config
16
+ });
17
+ for (const e of resolved) {
18
+ entities.set(e.id, e);
19
+ }
20
+ }
21
+ return { ...raw, entities };
22
+ }
23
+ export {
24
+ getFansUnitedSportsCompetitionHydrated
25
+ };
26
+ //# sourceMappingURL=hydrated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrated.js","sources":["../../../../../src/lib/api/fansunited/sports/competition/hydrated.ts"],"sourcesContent":["/**\n * Hydrated wrapper for the Fans United Sports API competition endpoint.\n *\n * Composes the (cached) raw competition response with a (cached) batch\n * resolution of every entity ID in `meta.entityIds` via the Search API,\n * and returns one object the consumer can render directly.\n *\n * Cache topology:\n * - sports/competition response → cached under \"sportsCompetition\"\n * (5min stale / 1h max) by id+season.\n * - search/entity responses → cached under \"search\" (24h stale / 7d max)\n * by id only; the raw multi-locale payload serves every locale via\n * transformEntity(raw, lang).\n *\n * @example\n * const data = await getFansUnitedSportsCompetitionHydrated(\"fb:c:1\", { locale: \"BG\" });\n * for (const entry of data.season.stages[0].groups[0].standings) {\n * const team = data.entities.get(entry.competitorId);\n * console.log(`${entry.rank} ${team?.name}`);\n * }\n */\n\nimport type { FUSportsCompetitionDetails } from \"../../../../types/canonical/sports-competition.types\";\nimport type { DataLayerConfig } from \"../../../../config\";\nimport type { NextCacheOptions } from \"../../http\";\nimport type { SearchEntityResult } from \"../../search/types\";\nimport { getFansUnitedSportsCompetition } from \"./index\";\nimport { getFansUnitedEntitiesByIds } from \"../../search\";\n\nexport interface GetSportsCompetitionHydratedOptions {\n /** Season identifier (e.g. \"fb:c:1:2025/26\"). Defaults to the active season. */\n seasonId?: string;\n /** Locale code (e.g. \"EN\", \"BG\") used to resolve entity translations. Falls back to EN. */\n locale?: string;\n /** Next.js cache options for ISR/on-demand revalidation. */\n next?: NextCacheOptions;\n}\n\n/**\n * Sports API competition response plus a lookup map for every referenced entity.\n *\n * The map is keyed by entity ID. Missing IDs (404s from the Search API) are\n * silently omitted — render code should fall back gracefully on a missing entry:\n * `entities.get(id) ?? { name: id }`.\n */\nexport interface FUSportsCompetitionHydrated extends FUSportsCompetitionDetails {\n entities: Map<string, SearchEntityResult>;\n}\n\nexport async function getFansUnitedSportsCompetitionHydrated(\n competitionId: string,\n options?: GetSportsCompetitionHydratedOptions,\n config?: DataLayerConfig\n): Promise<FUSportsCompetitionHydrated> {\n const raw = await getFansUnitedSportsCompetition(\n competitionId,\n { seasonId: options?.seasonId, next: options?.next },\n config\n );\n\n const entityIds = raw.meta.entityIds;\n const entities = new Map<string, SearchEntityResult>();\n\n if (entityIds.length > 0) {\n const resolved = await getFansUnitedEntitiesByIds(entityIds, {\n lang: options?.locale,\n next: options?.next,\n config,\n });\n for (const e of resolved) {\n entities.set(e.id, e);\n }\n }\n\n return { ...raw, entities };\n}\n"],"names":[],"mappings":";;AAiDA,eAAsB,uCAClB,eACA,SACA,QACoC;AACpC,QAAM,MAAM,MAAM;AAAA,IACd;AAAA,IACA,EAAE,UAAU,SAAS,UAAU,MAAM,SAAS,KAAA;AAAA,IAC9C;AAAA,EAAA;AAGJ,QAAM,YAAY,IAAI,KAAK;AAC3B,QAAM,+BAAe,IAAA;AAErB,MAAI,UAAU,SAAS,GAAG;AACtB,UAAM,WAAW,MAAM,2BAA2B,WAAW;AAAA,MACzD,MAAM,SAAS;AAAA,MACf,MAAM,SAAS;AAAA,MACf;AAAA,IAAA,CACH;AACD,eAAW,KAAK,UAAU;AACtB,eAAS,IAAI,EAAE,IAAI,CAAC;AAAA,IACxB;AAAA,EACJ;AAEA,SAAO,EAAE,GAAG,KAAK,SAAA;AACrB;"}
@@ -8,21 +8,12 @@ export type { GetSportsCompetitionOptions } from "./types";
8
8
  /**
9
9
  * Get a competition by ID from the Fans United Sports API.
10
10
  *
11
+ * Cached with SWR under entity type "sportsCompetition" (5min stale / 1h max).
12
+ * Cache key: `fudl:sports:competition:<id>:<seasonId | "active">`.
13
+ *
11
14
  * @param competitionId - Competition identifier (e.g. "fb:c:1")
12
15
  * @param options - Query options (season override, Next.js cache hints)
13
16
  * @param config - Optional data layer config (uses the singleton if omitted)
14
- *
15
- * @example
16
- * ```typescript
17
- * const competition = await getFansUnitedSportsCompetition("fb:c:1");
18
- * for (const stage of competition.season.stages) {
19
- * if (stage.type === "standing") {
20
- * for (const group of stage.groups ?? []) {
21
- * console.log(group.standings.map(s => `${s.rank} ${s.competitorId}`));
22
- * }
23
- * }
24
- * }
25
- * ```
26
17
  */
27
18
  export declare function getFansUnitedSportsCompetition(competitionId: string, options?: GetSportsCompetitionOptions, config?: DataLayerConfig): Promise<FUSportsCompetitionDetails>;
28
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/api/fansunited/sports/competition/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sDAAsD,CAAC;AACvG,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrE,OAAO,KAAK,EAAE,2BAA2B,EAAgC,MAAM,SAAS,CAAC;AAEzF,YAAY,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAE3D;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,8BAA8B,CAChD,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,2BAA2B,EACrC,MAAM,CAAC,EAAE,eAAe,GACzB,OAAO,CAAC,0BAA0B,CAAC,CAgBrC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/api/fansunited/sports/competition/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sDAAsD,CAAC;AACvG,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAIrE,OAAO,KAAK,EAAE,2BAA2B,EAAgC,MAAM,SAAS,CAAC;AAEzF,YAAY,EAAE,2BAA2B,EAAE,MAAM,SAAS,CAAC;AAE3D;;;;;;;;;GASG;AACH,wBAAsB,8BAA8B,CAChD,aAAa,EAAE,MAAM,EACrB,OAAO,CAAC,EAAE,2BAA2B,EACrC,MAAM,CAAC,EAAE,eAAe,GACzB,OAAO,CAAC,0BAA0B,CAAC,CAoBrC"}
@@ -1,19 +1,25 @@
1
1
  import { getConfig } from "../../../../config/index.js";
2
2
  import { sportsHttp } from "../http.js";
3
3
  import { transformSportsCompetition } from "./transformer.js";
4
+ import { cached } from "../../../../cache/cache-manager.js";
5
+ import { CACHE_KEY_PREFIX } from "../../../../cache/types.js";
4
6
  async function getFansUnitedSportsCompetition(competitionId, options, config) {
5
7
  const finalConfig = config || getConfig();
6
- const params = {};
7
- if (options?.seasonId) {
8
- params.season_id = options.seasonId;
9
- }
10
- const raw = await sportsHttp.get({
11
- path: `/v1/competitions/${competitionId}`,
12
- params,
13
- next: options?.next,
14
- config: finalConfig
8
+ const seasonKeyPart = options?.seasonId ?? "active";
9
+ const cacheKey = `${CACHE_KEY_PREFIX}sports:competition:${competitionId}:${seasonKeyPart}`;
10
+ return cached(cacheKey, "sportsCompetition", async () => {
11
+ const params = {};
12
+ if (options?.seasonId) {
13
+ params.season_id = options.seasonId;
14
+ }
15
+ const raw = await sportsHttp.get({
16
+ path: `/v1/competitions/${competitionId}`,
17
+ params,
18
+ next: options?.next,
19
+ config: finalConfig
20
+ });
21
+ return transformSportsCompetition(raw);
15
22
  });
16
- return transformSportsCompetition(raw);
17
23
  }
18
24
  export {
19
25
  getFansUnitedSportsCompetition
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../../../../src/lib/api/fansunited/sports/competition/index.ts"],"sourcesContent":["/**\n * Competition endpoint for the Fans United Sports API.\n */\n\nimport type { FUSportsCompetitionDetails } from \"../../../../types/canonical/sports-competition.types\";\nimport { getConfig, type DataLayerConfig } from \"../../../../config\";\nimport { sportsHttp } from \"../http\";\nimport { transformSportsCompetition } from \"./transformer\";\nimport type { GetSportsCompetitionOptions, RawSportsCompetitionResponse } from \"./types\";\n\nexport type { GetSportsCompetitionOptions } from \"./types\";\n\n/**\n * Get a competition by ID from the Fans United Sports API.\n *\n * @param competitionId - Competition identifier (e.g. \"fb:c:1\")\n * @param options - Query options (season override, Next.js cache hints)\n * @param config - Optional data layer config (uses the singleton if omitted)\n *\n * @example\n * ```typescript\n * const competition = await getFansUnitedSportsCompetition(\"fb:c:1\");\n * for (const stage of competition.season.stages) {\n * if (stage.type === \"standing\") {\n * for (const group of stage.groups ?? []) {\n * console.log(group.standings.map(s => `${s.rank} ${s.competitorId}`));\n * }\n * }\n * }\n * ```\n */\nexport async function getFansUnitedSportsCompetition(\n competitionId: string,\n options?: GetSportsCompetitionOptions,\n config?: DataLayerConfig\n): Promise<FUSportsCompetitionDetails> {\n const finalConfig = config || getConfig();\n\n const params: Record<string, string | undefined> = {};\n if (options?.seasonId) {\n params.season_id = options.seasonId;\n }\n\n const raw = await sportsHttp.get<RawSportsCompetitionResponse>({\n path: `/v1/competitions/${competitionId}`,\n params,\n next: options?.next,\n config: finalConfig,\n });\n\n return transformSportsCompetition(raw);\n}\n"],"names":[],"mappings":";;;AA+BA,eAAsB,+BAClB,eACA,SACA,QACmC;AACnC,QAAM,cAAc,UAAU,UAAA;AAE9B,QAAM,SAA6C,CAAA;AACnD,MAAI,SAAS,UAAU;AACnB,WAAO,YAAY,QAAQ;AAAA,EAC/B;AAEA,QAAM,MAAM,MAAM,WAAW,IAAkC;AAAA,IAC3D,MAAM,oBAAoB,aAAa;AAAA,IACvC;AAAA,IACA,MAAM,SAAS;AAAA,IACf,QAAQ;AAAA,EAAA,CACX;AAED,SAAO,2BAA2B,GAAG;AACzC;"}
1
+ {"version":3,"file":"index.js","sources":["../../../../../src/lib/api/fansunited/sports/competition/index.ts"],"sourcesContent":["/**\n * Competition endpoint for the Fans United Sports API.\n */\n\nimport type { FUSportsCompetitionDetails } from \"../../../../types/canonical/sports-competition.types\";\nimport { getConfig, type DataLayerConfig } from \"../../../../config\";\nimport { cached, CACHE_KEY_PREFIX } from \"../../../../cache\";\nimport { sportsHttp } from \"../http\";\nimport { transformSportsCompetition } from \"./transformer\";\nimport type { GetSportsCompetitionOptions, RawSportsCompetitionResponse } from \"./types\";\n\nexport type { GetSportsCompetitionOptions } from \"./types\";\n\n/**\n * Get a competition by ID from the Fans United Sports API.\n *\n * Cached with SWR under entity type \"sportsCompetition\" (5min stale / 1h max).\n * Cache key: `fudl:sports:competition:<id>:<seasonId | \"active\">`.\n *\n * @param competitionId - Competition identifier (e.g. \"fb:c:1\")\n * @param options - Query options (season override, Next.js cache hints)\n * @param config - Optional data layer config (uses the singleton if omitted)\n */\nexport async function getFansUnitedSportsCompetition(\n competitionId: string,\n options?: GetSportsCompetitionOptions,\n config?: DataLayerConfig\n): Promise<FUSportsCompetitionDetails> {\n const finalConfig = config || getConfig();\n const seasonKeyPart = options?.seasonId ?? \"active\";\n const cacheKey = `${CACHE_KEY_PREFIX}sports:competition:${competitionId}:${seasonKeyPart}`;\n\n return cached(cacheKey, \"sportsCompetition\", async () => {\n const params: Record<string, string | undefined> = {};\n if (options?.seasonId) {\n params.season_id = options.seasonId;\n }\n\n const raw = await sportsHttp.get<RawSportsCompetitionResponse>({\n path: `/v1/competitions/${competitionId}`,\n params,\n next: options?.next,\n config: finalConfig,\n });\n\n return transformSportsCompetition(raw);\n });\n}\n"],"names":[],"mappings":";;;;;AAuBA,eAAsB,+BAClB,eACA,SACA,QACmC;AACnC,QAAM,cAAc,UAAU,UAAA;AAC9B,QAAM,gBAAgB,SAAS,YAAY;AAC3C,QAAM,WAAW,GAAG,gBAAgB,sBAAsB,aAAa,IAAI,aAAa;AAExF,SAAO,OAAO,UAAU,qBAAqB,YAAY;AACrD,UAAM,SAA6C,CAAA;AACnD,QAAI,SAAS,UAAU;AACnB,aAAO,YAAY,QAAQ;AAAA,IAC/B;AAEA,UAAM,MAAM,MAAM,WAAW,IAAkC;AAAA,MAC3D,MAAM,oBAAoB,aAAa;AAAA,MACvC;AAAA,MACA,MAAM,SAAS;AAAA,MACf,QAAQ;AAAA,IAAA,CACX;AAED,WAAO,2BAA2B,GAAG;AAAA,EACzC,CAAC;AACL;"}
@@ -6,4 +6,10 @@
6
6
  */
7
7
  export { getFansUnitedSportsCompetition } from "./competition";
8
8
  export type { GetSportsCompetitionOptions } from "./competition/types";
9
+ export { getFansUnitedSportsLivescore } from "./livescore";
10
+ export type { GetSportsLivescoreOptions } from "./livescore/types";
11
+ export { getFansUnitedSportsCompetitionHydrated } from "./competition/hydrated";
12
+ export type { FUSportsCompetitionHydrated, GetSportsCompetitionHydratedOptions } from "./competition/hydrated";
13
+ export { getFansUnitedSportsLivescoreHydrated } from "./livescore/hydrated";
14
+ export type { FUSportsLivescoreHydrated, GetSportsLivescoreHydratedOptions } from "./livescore/hydrated";
9
15
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/api/fansunited/sports/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,8BAA8B,EAAE,MAAM,eAAe,CAAC;AAC/D,YAAY,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/api/fansunited/sports/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,8BAA8B,EAAE,MAAM,eAAe,CAAC;AAC/D,YAAY,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAEvE,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAC;AAC3D,YAAY,EAAE,yBAAyB,EAAE,MAAM,mBAAmB,CAAC;AAEnE,OAAO,EAAE,sCAAsC,EAAE,MAAM,wBAAwB,CAAC;AAChF,YAAY,EAAE,2BAA2B,EAAE,mCAAmC,EAAE,MAAM,wBAAwB,CAAC;AAE/G,OAAO,EAAE,oCAAoC,EAAE,MAAM,sBAAsB,CAAC;AAC5E,YAAY,EAAE,yBAAyB,EAAE,iCAAiC,EAAE,MAAM,sBAAsB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=hydrated.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrated.test.d.ts","sourceRoot":"","sources":["../../../../../../src/lib/api/fansunited/sports/livescore/__tests__/hydrated.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Hydrated wrapper for the Fans United Sports API livescore endpoint.
3
+ *
4
+ * Returns the raw livescore payload merged with a lookup map of every
5
+ * referenced entity (competitions + competitors), resolved through the
6
+ * Search API.
7
+ *
8
+ * Cache topology — different from {@link getFansUnitedSportsCompetitionHydrated}:
9
+ * - livescore response → **not** cached. Live scores change minute-by-minute,
10
+ * and the design spec keeps live data on a client-side polling channel
11
+ * rather than the SWR cache. Each call hits the livescore API fresh.
12
+ * - search/entity responses → cached under "search" (24h stale / 7d max)
13
+ * by id only; the raw multi-locale payload serves every locale via
14
+ * transformEntity(raw, lang).
15
+ *
16
+ * The net effect: repeated calls to this function each make one livescore
17
+ * request, but reuse already-fetched entity payloads across calls and locales.
18
+ *
19
+ * @example
20
+ * const board = await getFansUnitedSportsLivescoreHydrated({ sport: "FOOTBALL", locale: "BG" });
21
+ * for (const group of board.data) {
22
+ * const comp = board.entities.get(group.competitionId);
23
+ * console.log(`${comp?.name} — ${group.events.length} events`);
24
+ * for (const event of group.events) {
25
+ * const home = board.entities.get(event.competitorOne.id);
26
+ * const away = board.entities.get(event.competitorTwo.id);
27
+ * console.log(` ${home?.name} vs ${away?.name}`);
28
+ * }
29
+ * }
30
+ */
31
+ import type { FUSportsLivescore } from "../../../../types/canonical/sports-livescore.types";
32
+ import type { DataLayerConfig } from "../../../../config";
33
+ import type { SearchEntityResult } from "../../search/types";
34
+ import type { GetSportsLivescoreOptions } from "./types";
35
+ export interface GetSportsLivescoreHydratedOptions extends GetSportsLivescoreOptions {
36
+ /** Locale code (e.g. "EN", "BG") used to resolve entity translations. Falls back to EN. */
37
+ locale?: string;
38
+ }
39
+ /**
40
+ * Livescore payload plus a lookup map for every referenced entity
41
+ * (competitions + competitors). Missing IDs are silently omitted — render
42
+ * code should fall back gracefully: `entities.get(id) ?? { name: id }`.
43
+ */
44
+ export interface FUSportsLivescoreHydrated extends FUSportsLivescore {
45
+ entities: Map<string, SearchEntityResult>;
46
+ }
47
+ export declare function getFansUnitedSportsLivescoreHydrated(options?: GetSportsLivescoreHydratedOptions, config?: DataLayerConfig): Promise<FUSportsLivescoreHydrated>;
48
+ //# sourceMappingURL=hydrated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrated.d.ts","sourceRoot":"","sources":["../../../../../src/lib/api/fansunited/sports/livescore/hydrated.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oDAAoD,CAAC;AAC5F,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAG7D,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AAEzD,MAAM,WAAW,iCAAkC,SAAQ,yBAAyB;IAChF,2FAA2F;IAC3F,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAA0B,SAAQ,iBAAiB;IAChE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAAC;CAC7C;AAiBD,wBAAsB,oCAAoC,CACtD,OAAO,CAAC,EAAE,iCAAiC,EAC3C,MAAM,CAAC,EAAE,eAAe,GACzB,OAAO,CAAC,yBAAyB,CAAC,CAmBpC"}
@@ -0,0 +1,34 @@
1
+ import { getFansUnitedSportsLivescore } from "./index.js";
2
+ import { getFansUnitedEntitiesByIds } from "../../search/index.js";
3
+ function collectEntityIds(livescore) {
4
+ const ids = /* @__PURE__ */ new Set();
5
+ for (const group of livescore.data) {
6
+ if (group.competitionId) ids.add(group.competitionId);
7
+ for (const event of group.events) {
8
+ if (event.competitorOne?.id) ids.add(event.competitorOne.id);
9
+ if (event.competitorTwo?.id) ids.add(event.competitorTwo.id);
10
+ }
11
+ }
12
+ return Array.from(ids);
13
+ }
14
+ async function getFansUnitedSportsLivescoreHydrated(options, config) {
15
+ const { locale, ...livescoreOptions } = options ?? {};
16
+ const raw = await getFansUnitedSportsLivescore(livescoreOptions, config);
17
+ const entityIds = collectEntityIds(raw);
18
+ const entities = /* @__PURE__ */ new Map();
19
+ if (entityIds.length > 0) {
20
+ const resolved = await getFansUnitedEntitiesByIds(entityIds, {
21
+ lang: locale,
22
+ next: livescoreOptions.next,
23
+ config
24
+ });
25
+ for (const e of resolved) {
26
+ entities.set(e.id, e);
27
+ }
28
+ }
29
+ return { ...raw, entities };
30
+ }
31
+ export {
32
+ getFansUnitedSportsLivescoreHydrated
33
+ };
34
+ //# sourceMappingURL=hydrated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hydrated.js","sources":["../../../../../src/lib/api/fansunited/sports/livescore/hydrated.ts"],"sourcesContent":["/**\n * Hydrated wrapper for the Fans United Sports API livescore endpoint.\n *\n * Returns the raw livescore payload merged with a lookup map of every\n * referenced entity (competitions + competitors), resolved through the\n * Search API.\n *\n * Cache topology — different from {@link getFansUnitedSportsCompetitionHydrated}:\n * - livescore response → **not** cached. Live scores change minute-by-minute,\n * and the design spec keeps live data on a client-side polling channel\n * rather than the SWR cache. Each call hits the livescore API fresh.\n * - search/entity responses → cached under \"search\" (24h stale / 7d max)\n * by id only; the raw multi-locale payload serves every locale via\n * transformEntity(raw, lang).\n *\n * The net effect: repeated calls to this function each make one livescore\n * request, but reuse already-fetched entity payloads across calls and locales.\n *\n * @example\n * const board = await getFansUnitedSportsLivescoreHydrated({ sport: \"FOOTBALL\", locale: \"BG\" });\n * for (const group of board.data) {\n * const comp = board.entities.get(group.competitionId);\n * console.log(`${comp?.name} — ${group.events.length} events`);\n * for (const event of group.events) {\n * const home = board.entities.get(event.competitorOne.id);\n * const away = board.entities.get(event.competitorTwo.id);\n * console.log(` ${home?.name} vs ${away?.name}`);\n * }\n * }\n */\n\nimport type { FUSportsLivescore } from \"../../../../types/canonical/sports-livescore.types\";\nimport type { DataLayerConfig } from \"../../../../config\";\nimport type { SearchEntityResult } from \"../../search/types\";\nimport { getFansUnitedSportsLivescore } from \"./index\";\nimport { getFansUnitedEntitiesByIds } from \"../../search\";\nimport type { GetSportsLivescoreOptions } from \"./types\";\n\nexport interface GetSportsLivescoreHydratedOptions extends GetSportsLivescoreOptions {\n /** Locale code (e.g. \"EN\", \"BG\") used to resolve entity translations. Falls back to EN. */\n locale?: string;\n}\n\n/**\n * Livescore payload plus a lookup map for every referenced entity\n * (competitions + competitors). Missing IDs are silently omitted — render\n * code should fall back gracefully: `entities.get(id) ?? { name: id }`.\n */\nexport interface FUSportsLivescoreHydrated extends FUSportsLivescore {\n entities: Map<string, SearchEntityResult>;\n}\n\n/**\n * Collect every entity ID referenced in the livescore response, deduplicated.\n */\nfunction collectEntityIds(livescore: FUSportsLivescore): string[] {\n const ids = new Set<string>();\n for (const group of livescore.data) {\n if (group.competitionId) ids.add(group.competitionId);\n for (const event of group.events) {\n if (event.competitorOne?.id) ids.add(event.competitorOne.id);\n if (event.competitorTwo?.id) ids.add(event.competitorTwo.id);\n }\n }\n return Array.from(ids);\n}\n\nexport async function getFansUnitedSportsLivescoreHydrated(\n options?: GetSportsLivescoreHydratedOptions,\n config?: DataLayerConfig\n): Promise<FUSportsLivescoreHydrated> {\n const { locale, ...livescoreOptions } = options ?? {};\n const raw = await getFansUnitedSportsLivescore(livescoreOptions, config);\n\n const entityIds = collectEntityIds(raw);\n const entities = new Map<string, SearchEntityResult>();\n\n if (entityIds.length > 0) {\n const resolved = await getFansUnitedEntitiesByIds(entityIds, {\n lang: locale,\n next: livescoreOptions.next,\n config,\n });\n for (const e of resolved) {\n entities.set(e.id, e);\n }\n }\n\n return { ...raw, entities };\n}\n"],"names":[],"mappings":";;AAuDA,SAAS,iBAAiB,WAAwC;AAC9D,QAAM,0BAAU,IAAA;AAChB,aAAW,SAAS,UAAU,MAAM;AAChC,QAAI,MAAM,cAAe,KAAI,IAAI,MAAM,aAAa;AACpD,eAAW,SAAS,MAAM,QAAQ;AAC9B,UAAI,MAAM,eAAe,QAAQ,IAAI,MAAM,cAAc,EAAE;AAC3D,UAAI,MAAM,eAAe,QAAQ,IAAI,MAAM,cAAc,EAAE;AAAA,IAC/D;AAAA,EACJ;AACA,SAAO,MAAM,KAAK,GAAG;AACzB;AAEA,eAAsB,qCAClB,SACA,QACkC;AAClC,QAAM,EAAE,QAAQ,GAAG,iBAAA,IAAqB,WAAW,CAAA;AACnD,QAAM,MAAM,MAAM,6BAA6B,kBAAkB,MAAM;AAEvE,QAAM,YAAY,iBAAiB,GAAG;AACtC,QAAM,+BAAe,IAAA;AAErB,MAAI,UAAU,SAAS,GAAG;AACtB,UAAM,WAAW,MAAM,2BAA2B,WAAW;AAAA,MACzD,MAAM;AAAA,MACN,MAAM,iBAAiB;AAAA,MACvB;AAAA,IAAA,CACH;AACD,eAAW,KAAK,UAAU;AACtB,eAAS,IAAI,EAAE,IAAI,CAAC;AAAA,IACxB;AAAA,EACJ;AAEA,SAAO,EAAE,GAAG,KAAK,SAAA;AACrB;"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Livescore endpoint for the Fans United Sports API.
3
+ */
4
+ import type { FUSportsLivescore } from "../../../../types/canonical/sports-livescore.types";
5
+ import { type DataLayerConfig } from "../../../../config";
6
+ import type { GetSportsLivescoreOptions } from "./types";
7
+ export type { GetSportsLivescoreOptions } from "./types";
8
+ /**
9
+ * Get live scores from the Fans United Sports API.
10
+ *
11
+ * Returns events for a given date grouped by competition, optionally filtered
12
+ * by sport, competitions, match IDs, and event status. Defaults to the current
13
+ * date (UTC) and all sports.
14
+ *
15
+ * @param options - Query filters and ordering (all optional)
16
+ * @param config - Optional data layer config (uses the singleton if omitted)
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const livescore = await getFansUnitedSportsLivescore({ sport: "FOOTBALL" });
21
+ * for (const group of livescore.data) {
22
+ * console.log(group.competitionId, group.events.length);
23
+ * }
24
+ * ```
25
+ */
26
+ export declare function getFansUnitedSportsLivescore(options?: GetSportsLivescoreOptions, config?: DataLayerConfig): Promise<FUSportsLivescore>;
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/api/fansunited/sports/livescore/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oDAAoD,CAAC;AAC5F,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAGrE,OAAO,KAAK,EAAE,yBAAyB,EAA8B,MAAM,SAAS,CAAC;AAErF,YAAY,EAAE,yBAAyB,EAAE,MAAM,SAAS,CAAC;AAEzD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,4BAA4B,CAC9C,OAAO,CAAC,EAAE,yBAAyB,EACnC,MAAM,CAAC,EAAE,eAAe,GACzB,OAAO,CAAC,iBAAiB,CAAC,CA+B5B"}
@@ -0,0 +1,36 @@
1
+ import { getConfig } from "../../../../config/index.js";
2
+ import { sportsHttp } from "../http.js";
3
+ import { transformSportsLivescore } from "./transformer.js";
4
+ async function getFansUnitedSportsLivescore(options, config) {
5
+ const finalConfig = config || getConfig();
6
+ const params = {};
7
+ if (options?.competitionIds?.length) {
8
+ params.competitions_ids = options.competitionIds.join(",");
9
+ }
10
+ if (options?.matchIds?.length) {
11
+ params.match_ids = options.matchIds.join(",");
12
+ }
13
+ if (options?.competitionsOrder?.length) {
14
+ params.competitions_order = options.competitionsOrder.join(",");
15
+ }
16
+ if (options?.date) {
17
+ params.date = options.date;
18
+ }
19
+ if (options?.eventStatus) {
20
+ params.event_status = options.eventStatus;
21
+ }
22
+ if (options?.sport) {
23
+ params.sport = options.sport;
24
+ }
25
+ const raw = await sportsHttp.get({
26
+ path: "/v1/livescore",
27
+ params,
28
+ next: options?.next,
29
+ config: finalConfig
30
+ });
31
+ return transformSportsLivescore(raw);
32
+ }
33
+ export {
34
+ getFansUnitedSportsLivescore
35
+ };
36
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sources":["../../../../../src/lib/api/fansunited/sports/livescore/index.ts"],"sourcesContent":["/**\n * Livescore endpoint for the Fans United Sports API.\n */\n\nimport type { FUSportsLivescore } from \"../../../../types/canonical/sports-livescore.types\";\nimport { getConfig, type DataLayerConfig } from \"../../../../config\";\nimport { sportsHttp } from \"../http\";\nimport { transformSportsLivescore } from \"./transformer\";\nimport type { GetSportsLivescoreOptions, RawSportsLivescoreResponse } from \"./types\";\n\nexport type { GetSportsLivescoreOptions } from \"./types\";\n\n/**\n * Get live scores from the Fans United Sports API.\n *\n * Returns events for a given date grouped by competition, optionally filtered\n * by sport, competitions, match IDs, and event status. Defaults to the current\n * date (UTC) and all sports.\n *\n * @param options - Query filters and ordering (all optional)\n * @param config - Optional data layer config (uses the singleton if omitted)\n *\n * @example\n * ```typescript\n * const livescore = await getFansUnitedSportsLivescore({ sport: \"FOOTBALL\" });\n * for (const group of livescore.data) {\n * console.log(group.competitionId, group.events.length);\n * }\n * ```\n */\nexport async function getFansUnitedSportsLivescore(\n options?: GetSportsLivescoreOptions,\n config?: DataLayerConfig\n): Promise<FUSportsLivescore> {\n const finalConfig = config || getConfig();\n\n const params: Record<string, string | undefined> = {};\n if (options?.competitionIds?.length) {\n params.competitions_ids = options.competitionIds.join(\",\");\n }\n if (options?.matchIds?.length) {\n params.match_ids = options.matchIds.join(\",\");\n }\n if (options?.competitionsOrder?.length) {\n params.competitions_order = options.competitionsOrder.join(\",\");\n }\n if (options?.date) {\n params.date = options.date;\n }\n if (options?.eventStatus) {\n params.event_status = options.eventStatus;\n }\n if (options?.sport) {\n params.sport = options.sport;\n }\n\n const raw = await sportsHttp.get<RawSportsLivescoreResponse>({\n path: \"/v1/livescore\",\n params,\n next: options?.next,\n config: finalConfig,\n });\n\n return transformSportsLivescore(raw);\n}\n"],"names":[],"mappings":";;;AA8BA,eAAsB,6BAClB,SACA,QAC0B;AAC1B,QAAM,cAAc,UAAU,UAAA;AAE9B,QAAM,SAA6C,CAAA;AACnD,MAAI,SAAS,gBAAgB,QAAQ;AACjC,WAAO,mBAAmB,QAAQ,eAAe,KAAK,GAAG;AAAA,EAC7D;AACA,MAAI,SAAS,UAAU,QAAQ;AAC3B,WAAO,YAAY,QAAQ,SAAS,KAAK,GAAG;AAAA,EAChD;AACA,MAAI,SAAS,mBAAmB,QAAQ;AACpC,WAAO,qBAAqB,QAAQ,kBAAkB,KAAK,GAAG;AAAA,EAClE;AACA,MAAI,SAAS,MAAM;AACf,WAAO,OAAO,QAAQ;AAAA,EAC1B;AACA,MAAI,SAAS,aAAa;AACtB,WAAO,eAAe,QAAQ;AAAA,EAClC;AACA,MAAI,SAAS,OAAO;AAChB,WAAO,QAAQ,QAAQ;AAAA,EAC3B;AAEA,QAAM,MAAM,MAAM,WAAW,IAAgC;AAAA,IACzD,MAAM;AAAA,IACN;AAAA,IACA,MAAM,SAAS;AAAA,IACf,QAAQ;AAAA,EAAA,CACX;AAED,SAAO,yBAAyB,GAAG;AACvC;"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Transforms the raw Sports API livescore response into the canonical
3
+ * {@link FUSportsLivescore} shape.
4
+ */
5
+ import type { FUSportsLivescore } from "../../../../types/canonical/sports-livescore.types";
6
+ import type { RawSportsLivescoreResponse } from "./types";
7
+ export declare function transformSportsLivescore(raw: RawSportsLivescoreResponse): FUSportsLivescore;
8
+ //# sourceMappingURL=transformer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformer.d.ts","sourceRoot":"","sources":["../../../../../src/lib/api/fansunited/sports/livescore/transformer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACR,iBAAiB,EAKpB,MAAM,oDAAoD,CAAC;AAC5D,OAAO,KAAK,EAGR,0BAA0B,EAG7B,MAAM,SAAS,CAAC;AAqCjB,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,0BAA0B,GAAG,iBAAiB,CAU3F"}
@@ -0,0 +1,44 @@
1
+ function transformStatus(raw) {
2
+ return {
3
+ type: raw.type,
4
+ code: raw.code,
5
+ subType: raw.sub_type
6
+ };
7
+ }
8
+ function transformScore(raw) {
9
+ return {
10
+ key: raw.key,
11
+ competitorOne: raw.competitor_one,
12
+ competitorTwo: raw.competitor_two
13
+ };
14
+ }
15
+ function transformEvent(raw) {
16
+ return {
17
+ id: raw.id,
18
+ status: transformStatus(raw.status),
19
+ score: raw.score ? raw.score.map(transformScore) : void 0,
20
+ competitorOne: { id: raw.competitor_one.id, type: raw.competitor_one.type },
21
+ competitorTwo: { id: raw.competitor_two.id, type: raw.competitor_two.type },
22
+ startTime: raw.start_time
23
+ };
24
+ }
25
+ function transformGroup(raw) {
26
+ return {
27
+ competitionId: raw.competition_id,
28
+ sport: raw.sport,
29
+ events: raw.events.map(transformEvent)
30
+ };
31
+ }
32
+ function transformSportsLivescore(raw) {
33
+ return {
34
+ data: raw.data.map(transformGroup),
35
+ meta: {
36
+ totalCompetitions: raw.meta?.total_competitions ?? raw.data.length,
37
+ totalEvents: raw.meta?.total_events ?? raw.data.reduce((sum, group) => sum + group.events.length, 0)
38
+ }
39
+ };
40
+ }
41
+ export {
42
+ transformSportsLivescore
43
+ };
44
+ //# sourceMappingURL=transformer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transformer.js","sources":["../../../../../src/lib/api/fansunited/sports/livescore/transformer.ts"],"sourcesContent":["/**\n * Transforms the raw Sports API livescore response into the canonical\n * {@link FUSportsLivescore} shape.\n */\n\nimport type {\n FUSportsLivescore,\n FUSportsLivescoreCompetitionGroup,\n FUSportsLivescoreEvent,\n FUSportsLivescoreScore,\n FUSportsLivescoreStatus,\n} from \"../../../../types/canonical/sports-livescore.types\";\nimport type {\n RawSportsLivescoreCompetitionGroup,\n RawSportsLivescoreEvent,\n RawSportsLivescoreResponse,\n RawSportsLivescoreScore,\n RawSportsLivescoreStatus,\n} from \"./types\";\n\nfunction transformStatus(raw: RawSportsLivescoreStatus): FUSportsLivescoreStatus {\n return {\n type: raw.type,\n code: raw.code,\n subType: raw.sub_type,\n };\n}\n\nfunction transformScore(raw: RawSportsLivescoreScore): FUSportsLivescoreScore {\n return {\n key: raw.key,\n competitorOne: raw.competitor_one,\n competitorTwo: raw.competitor_two,\n };\n}\n\nfunction transformEvent(raw: RawSportsLivescoreEvent): FUSportsLivescoreEvent {\n return {\n id: raw.id,\n status: transformStatus(raw.status),\n score: raw.score ? raw.score.map(transformScore) : undefined,\n competitorOne: { id: raw.competitor_one.id, type: raw.competitor_one.type },\n competitorTwo: { id: raw.competitor_two.id, type: raw.competitor_two.type },\n startTime: raw.start_time,\n };\n}\n\nfunction transformGroup(raw: RawSportsLivescoreCompetitionGroup): FUSportsLivescoreCompetitionGroup {\n return {\n competitionId: raw.competition_id,\n sport: raw.sport,\n events: raw.events.map(transformEvent),\n };\n}\n\nexport function transformSportsLivescore(raw: RawSportsLivescoreResponse): FUSportsLivescore {\n return {\n data: raw.data.map(transformGroup),\n meta: {\n totalCompetitions: raw.meta?.total_competitions ?? raw.data.length,\n totalEvents:\n raw.meta?.total_events ??\n raw.data.reduce((sum, group) => sum + group.events.length, 0),\n },\n };\n}\n"],"names":[],"mappings":"AAoBA,SAAS,gBAAgB,KAAwD;AAC7E,SAAO;AAAA,IACH,MAAM,IAAI;AAAA,IACV,MAAM,IAAI;AAAA,IACV,SAAS,IAAI;AAAA,EAAA;AAErB;AAEA,SAAS,eAAe,KAAsD;AAC1E,SAAO;AAAA,IACH,KAAK,IAAI;AAAA,IACT,eAAe,IAAI;AAAA,IACnB,eAAe,IAAI;AAAA,EAAA;AAE3B;AAEA,SAAS,eAAe,KAAsD;AAC1E,SAAO;AAAA,IACH,IAAI,IAAI;AAAA,IACR,QAAQ,gBAAgB,IAAI,MAAM;AAAA,IAClC,OAAO,IAAI,QAAQ,IAAI,MAAM,IAAI,cAAc,IAAI;AAAA,IACnD,eAAe,EAAE,IAAI,IAAI,eAAe,IAAI,MAAM,IAAI,eAAe,KAAA;AAAA,IACrE,eAAe,EAAE,IAAI,IAAI,eAAe,IAAI,MAAM,IAAI,eAAe,KAAA;AAAA,IACrE,WAAW,IAAI;AAAA,EAAA;AAEvB;AAEA,SAAS,eAAe,KAA4E;AAChG,SAAO;AAAA,IACH,eAAe,IAAI;AAAA,IACnB,OAAO,IAAI;AAAA,IACX,QAAQ,IAAI,OAAO,IAAI,cAAc;AAAA,EAAA;AAE7C;AAEO,SAAS,yBAAyB,KAAoD;AACzF,SAAO;AAAA,IACH,MAAM,IAAI,KAAK,IAAI,cAAc;AAAA,IACjC,MAAM;AAAA,MACF,mBAAmB,IAAI,MAAM,sBAAsB,IAAI,KAAK;AAAA,MAC5D,aACI,IAAI,MAAM,gBACV,IAAI,KAAK,OAAO,CAAC,KAAK,UAAU,MAAM,MAAM,OAAO,QAAQ,CAAC;AAAA,IAAA;AAAA,EACpE;AAER;"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Raw API response and option types for the Fans United Sports API
3
+ * livescore endpoint.
4
+ */
5
+ import type { NextCacheOptions } from "../../http";
6
+ import type { FUSportsLivescoreEventStatus, FUSportsLivescoreSport } from "../../../../types/canonical/sports-livescore.types";
7
+ export interface RawSportsLivescoreStatus {
8
+ type: string;
9
+ code: string;
10
+ sub_type: string;
11
+ }
12
+ export interface RawSportsLivescoreScore {
13
+ key: string;
14
+ competitor_one: string;
15
+ competitor_two: string;
16
+ }
17
+ export interface RawSportsLivescoreCompetitor {
18
+ id: string;
19
+ type: string;
20
+ }
21
+ export interface RawSportsLivescoreEvent {
22
+ id: string;
23
+ status: RawSportsLivescoreStatus;
24
+ score?: RawSportsLivescoreScore[];
25
+ competitor_one: RawSportsLivescoreCompetitor;
26
+ competitor_two: RawSportsLivescoreCompetitor;
27
+ start_time: string;
28
+ }
29
+ export interface RawSportsLivescoreCompetitionGroup {
30
+ competition_id: string;
31
+ sport: string;
32
+ events: RawSportsLivescoreEvent[];
33
+ }
34
+ export interface RawSportsLivescoreMeta {
35
+ total_competitions: number;
36
+ total_events: number;
37
+ }
38
+ export interface RawSportsLivescoreResponse {
39
+ data: RawSportsLivescoreCompetitionGroup[];
40
+ meta: RawSportsLivescoreMeta;
41
+ }
42
+ export interface GetSportsLivescoreOptions {
43
+ /** Comma-joined or array of competition IDs to filter by. */
44
+ competitionIds?: string[];
45
+ /** Match IDs to filter by. */
46
+ matchIds?: string[];
47
+ /** Competition IDs defining the order of competition groups in the response. */
48
+ competitionsOrder?: string[];
49
+ /** Date in yyyy-MM-dd format. Defaults to the current date (UTC). */
50
+ date?: string;
51
+ /** Filter by event status. */
52
+ eventStatus?: FUSportsLivescoreEventStatus;
53
+ /** Filter by sport. */
54
+ sport?: FUSportsLivescoreSport;
55
+ /** Next.js cache options for ISR/on-demand revalidation. */
56
+ next?: NextCacheOptions;
57
+ }
58
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../../src/lib/api/fansunited/sports/livescore/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,KAAK,EACR,4BAA4B,EAC5B,sBAAsB,EACzB,MAAM,oDAAoD,CAAC;AAM5D,MAAM,WAAW,wBAAwB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,MAAM,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,4BAA4B;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,uBAAuB;IACpC,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,wBAAwB,CAAC;IACjC,KAAK,CAAC,EAAE,uBAAuB,EAAE,CAAC;IAClC,cAAc,EAAE,4BAA4B,CAAC;IAC7C,cAAc,EAAE,4BAA4B,CAAC;IAC7C,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kCAAkC;IAC/C,cAAc,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,uBAAuB,EAAE,CAAC;CACrC;AAED,MAAM,WAAW,sBAAsB;IACnC,kBAAkB,EAAE,MAAM,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,0BAA0B;IACvC,IAAI,EAAE,kCAAkC,EAAE,CAAC;IAC3C,IAAI,EAAE,sBAAsB,CAAC;CAChC;AAMD,MAAM,WAAW,yBAAyB;IACtC,6DAA6D;IAC7D,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,gFAAgF;IAChF,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,qEAAqE;IACrE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8BAA8B;IAC9B,WAAW,CAAC,EAAE,4BAA4B,CAAC;IAC3C,uBAAuB;IACvB,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,4DAA4D;IAC5D,IAAI,CAAC,EAAE,gBAAgB,CAAC;CAC3B"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Redis L2 integration tests. Skipped unless REDIS_HOST is set, so the
3
+ * default `npm test` run stays hermetic.
4
+ *
5
+ * To run:
6
+ * docker compose up -d redis
7
+ * REDIS_HOST=redis npm test -- --run src/lib/cache/__tests__/redis-integration.test.ts
8
+ *
9
+ * From a host shell (Redis exposed on localhost:6379):
10
+ * docker compose up -d redis
11
+ * REDIS_HOST=localhost npm test -- --run src/lib/cache/__tests__/redis-integration.test.ts
12
+ */
13
+ export {};
14
+ //# sourceMappingURL=redis-integration.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-integration.test.d.ts","sourceRoot":"","sources":["../../../src/lib/cache/__tests__/redis-integration.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Reference Redis L2 adapter — kept under __tests__/ so it ships only with
3
+ * the dev install (ioredis is a devDependency). Consumers copy this verbatim
4
+ * (or adapt) into their own codebase per the design spec's "Decision 2":
5
+ * the Redis client is consumer-supplied so the data-layer stays browser-safe.
6
+ *
7
+ * Implementation notes
8
+ * --------------------
9
+ *
10
+ * 1. **Cache envelope.** Each value is a JSON-encoded `CacheEntry<T>`
11
+ * (`{ data, storedAt }`). The `storedAt` timestamp is what the cache
12
+ * manager uses to decide freshness/expiry — Redis's own TTL is a memory
13
+ * safety net, not the source of truth for SWR semantics.
14
+ *
15
+ * 2. **Eviction TTL.** Every `set`/`setMany` issues an `EX` matching
16
+ * `getEntityTTL(entity).maxTTL`. That keeps Redis from accumulating dead
17
+ * entries without needing a periodic cleanup scan. The cache manager
18
+ * still defensively rejects entries past `maxTTL` on read via
19
+ * `isExpired(entry, maxTTL)`, so the two layers can drift slightly
20
+ * without correctness issues.
21
+ *
22
+ * 3. **Graceful degradation.** The spec requires that a degraded L2 must
23
+ * never throw to the caller. We:
24
+ * - report `isInitialized` based on the ioredis status (only `"ready"`
25
+ * qualifies);
26
+ * - swallow read errors and return `undefined`;
27
+ * - swallow write errors and no-op.
28
+ * The cache manager will then fall back to L1 + API.
29
+ *
30
+ * 4. **`clear()` namespacing.** Uses `SCAN MATCH <prefix>*` + pipelined
31
+ * `DEL`. Other keys in the same Redis instance are untouched, which is
32
+ * what the `fudl:` prefix is for in the first place.
33
+ */
34
+ import type { Redis } from "ioredis";
35
+ import type { L2Store } from "../cache-manager";
36
+ export interface RedisL2StoreOptions {
37
+ /** Connected (or auto-connecting) ioredis client. */
38
+ client: Redis;
39
+ /**
40
+ * Override the prefix used by `clear()` for SCAN. Defaults to the
41
+ * data-layer's CACHE_KEY_PREFIX so consumers can clear the data-layer's
42
+ * keys without touching unrelated data in a shared Redis instance.
43
+ */
44
+ clearPrefix?: string;
45
+ /**
46
+ * Optional logger. Defaults to console. Swap for your structured logger
47
+ * in production so failures don't get lost in stdout.
48
+ */
49
+ log?: (level: "warn" | "error", msg: string, err?: unknown) => void;
50
+ }
51
+ export declare function createRedisL2Store(options: RedisL2StoreOptions): L2Store;
52
+ //# sourceMappingURL=redis-l2-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis-l2-store.d.ts","sourceRoot":"","sources":["../../../src/lib/cache/__tests__/redis-l2-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAUhD,MAAM,WAAW,mBAAmB;IAChC,qDAAqD;IACrD,MAAM,EAAE,KAAK,CAAC;IACd;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CACvE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAkFxE"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Async in-memory L2 stub. Used by tests that exercise the async L2 path
3
+ * (Redis adapters are async). Filename intentionally lacks `.test.ts` so
4
+ * vitest's `src/**\/*.test.ts` glob does not pick it up.
5
+ */
6
+ import type { L2Store } from "../cache-manager";
7
+ import type { EntityType } from "../types";
8
+ interface StoredRow {
9
+ entity: EntityType;
10
+ data: unknown;
11
+ storedAt: number;
12
+ }
13
+ export declare function createTestL2Store(): L2Store & {
14
+ reset(): void;
15
+ raw: Map<string, StoredRow>;
16
+ };
17
+ export {};
18
+ //# sourceMappingURL=test-l2-store.d.ts.map