-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.mjs
More file actions
111 lines (103 loc) · 4.85 KB
/
Copy pathserver.mjs
File metadata and controls
111 lines (103 loc) · 4.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { dirname, extname, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
const root = dirname(fileURLToPath(import.meta.url));
const port = Number(process.env.PORT || 4185);
const mime = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8", ".mjs": "text/javascript; charset=utf-8", ".png": "image/png", ".svg": "image/svg+xml" };
let cache = { expiresAt: 0, value: null };
async function readEnvFile(path) {
try {
const text = await readFile(path, "utf8");
return Object.fromEntries(text.split(/\r?\n/).filter((line) => line && !line.startsWith("#")).map((line) => {
const index = line.indexOf("=");
return [line.slice(0, index), line.slice(index + 1)];
}));
} catch {
return {};
}
}
async function credentials() {
const local = await readEnvFile(resolve(root, ".env"));
const workspace = await readEnvFile(resolve(root, "..", ".env"));
const values = { ...workspace, ...local, ...process.env };
if (!values.TXLINE_JWT || !values.TXLINE_API_TOKEN) throw new Error("TxLINE credentials are not configured");
const origin = values.TXLINE_API_ORIGIN || "https://txline.txodds.com";
if (origin !== "https://txline.txodds.com") throw new Error("Unsupported TxLINE origin");
return { ...values, origin };
}
async function txline(path, auth) {
const response = await fetch(`${auth.origin}${path}`, {
headers: { authorization: `Bearer ${auth.TXLINE_JWT}`, "x-api-token": auth.TXLINE_API_TOKEN, accept: "application/json" },
signal: AbortSignal.timeout(15_000),
});
if (!response.ok) throw new Error(`TxLINE ${response.status} for ${path.split("?")[0]}`);
return response.json();
}
function normalizeMarkets(rows) {
return (Array.isArray(rows) ? rows : [])
.filter((row) => String(row.Bookmaker || "").includes("StablePrice"))
.map((row) => ({
type: row.SuperOddsType,
period: row.MarketPeriod || "Match",
parameters: row.MarketParameters || "",
timestamp: Number(row.Ts),
inRunning: Boolean(row.InRunning),
selections: (row.PriceNames || []).map((name, index) => ({
name,
probability: Number(row.Pct?.[index]),
price: Number(row.Prices?.[index]) / 1000,
})).filter((item) => Number.isFinite(item.probability)),
}));
}
async function liveSnapshot() {
if (cache.value && cache.expiresAt > Date.now()) return cache.value;
const auth = await credentials();
const rows = await txline("/api/fixtures/snapshot", auth);
const now = Date.now();
const relevant = (Array.isArray(rows) ? rows : [])
.sort((a, b) => Math.abs(Number(a.StartTime) - now) - Math.abs(Number(b.StartTime) - now))
.slice(0, 8);
const settled = await Promise.all(relevant.map(async (fixture) => {
const current = await txline(`/api/odds/snapshot/${fixture.FixtureId}`, auth);
const previous = current.length ? await txline(`/api/odds/snapshot/${fixture.FixtureId}?asOf=${now - 90_000}`, auth) : [];
const markets = normalizeMarkets(current);
return {
id: fixture.FixtureId,
competition: fixture.Competition || "World Cup",
participant1: fixture.Participant1,
participant2: fixture.Participant2,
participant1IsHome: Boolean(fixture.Participant1IsHome),
startTime: fixture.StartTime,
gameState: fixture.GameState,
live: markets.some((market) => market.inRunning),
markets,
previousMarkets: normalizeMarkets(previous),
};
}));
const value = { source: "txline", asOf: new Date(now).toISOString(), fixtures: settled.filter((fixture) => fixture.markets.length) };
cache = { expiresAt: now + 10_000, value };
return value;
}
function json(response, status, payload) {
response.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
response.end(JSON.stringify(payload));
}
createServer(async (request, response) => {
const url = new URL(request.url, "http://localhost");
if (url.pathname === "/api/live") {
try { json(response, 200, await liveSnapshot()); }
catch (error) { json(response, 503, { error: error.message }); }
return;
}
const relative = url.pathname === "/" ? "index.html" : decodeURIComponent(url.pathname).replace(/^\/+/, "");
const filePath = resolve(root, relative);
if (!filePath.startsWith(`${root}${sep}`)) { response.writeHead(403).end("Forbidden"); return; }
try {
const file = await readFile(filePath);
response.writeHead(200, { "Content-Type": mime[extname(filePath)] || "application/octet-stream", "Cache-Control": extname(filePath) === ".html" ? "no-store" : "public, max-age=3600" });
response.end(file);
} catch {
response.writeHead(404).end("Not found");
}
}).listen(port, "127.0.0.1", () => console.log(`Roarcast is available at http://127.0.0.1:${port}`));