Skip to content

cuppachino/hexgate

Repository files navigation

Hexgate

Discord Codacy Badge Release License npm (scoped)

Hexgate is a work-in-progress LCU suite. It is not endorsed by Riot Games. You can find out more about what that means here. Thank you Riot ❤️ for keeping the LCU open. If you have any questions, feel free to join the cuppachino discord.

Please refer to the wiki for more info.

Installation

Add it to your own project using your favorite package manager.

pnpm add hexgate
npm i hexgate
yarn add hexgate

ESM

import { ... } from "hexgate"

CJS

import hexgate = require("hexgate")
const { ... } = hexgate

Authentication

Wait for the client by passing the auth function to the poll utility.

import { auth, poll } from "hexgate"

const credentials = await poll(auth)

Opt-out of safe authentication by explicity passing an undefined certifcate.

const unsafeCredentials = await auth({ certificate: undefined })

Once you have the credentials, you can create a new Hexgate and LcuClient.

import { Hexgate as HttpsClient, LcuClient as WsClient } from "hexgate"

const httpsClient = new HttpsClient(credentials)
const websocketClient = new WsClient(credentials)

Working with multiple clients? Get get all credentials.

import { auth, createHexgate, createLcuClient, poll, zip } from 'hexgate'

const credentials = await poll(() => auth({ all: true }))

// ~ some multi-connection interface
const clients = new Set(
  zip(
    credentials.map(createHexgate),
    credentials.map(createLcuClient)
  )
)

Builder API

The simplest way of getting started is to ".build" a request function. The builder uses generics to infer the parameters and return type of the request.

import { Hexgate as HttpsClient } from 'hexgate'

const https = new HttpsClient(credentials)

// (arg: string[], init?: any) => Promise<ApiResponse<{ ... }>>
const getSummonersFromNames = https
  .build('/lol-summoner/v2/summoners/names')
  .method('post')
  .create()

const summoner = await getSummonersByName(['dubbleignite'])
console.log(summoner.data)

Websocket Events

Subscribe to LCU events through the client.

import { LcuClient as WsClient } from 'hexgate'

const ws = new WsClient(credentials)

ws.subscribe(
  'OnJsonApiEvent_lol-champ-select_v1_session',
  ({ data, eventType, uri }) => {
    // side effects
  }
)

Note: Since many endpoints will subscribe you to multiple uris, its difficult to provide meaningful type inference for the data property. Import LcuComponents type when necessary and/or open a PR to improve typings - which would be greatly appreciated! I'm just improving types as I need them.

⚡️ Connection

The Connection class further abstracts Hexgate & LcuClient and handles authentication between client shutdowns. Configuration is optional.

import { Connection } from 'hexgate'

const client = new Connection({
  // Recipe API (createRecipe or recipe)
  createRecipe({ build, unwrap }) {
    return {
      getCurrentSummoner: unwrap(
        build('/lol-summoner/v1/current-summoner').method('get').create()
      )
    }
  },
  // Propagate status to browser windows.
  onStatusChange(status) {
    emit('status', status)
  },
  // Init
  async onConnect(con) {
    con.ws.subscribe('OnJsonApiEvent_lol-champ-select_v1_session', handleChampSelect)
    const summoner = await con.recipe.getCurrentSummoner()
    con.logger.info(summoner, `Welcome, ${summoner.displayName}`)
  },
  // Automatically reconnect
  async onDisconnect(discon) {
    await sleep(4000)
    discon.connect()
  },
  // Authentication interval
  interval: 2000,
  // Bring any logger
  logger: pino({
    name: 'main' as const
  })
})

client.connect()

The Connection class supports recipes, define a recipe: Recipe or a createRecipe: RecipeApiFn method in the ConnectionConfig constructor argument.

import { Connection, createRecipe } from 'hexgate'

const recipe = createRecipe(({ build }) => ({/*...*/}))
const client = new Connection({
  recipe
})
import { Connection } from 'hexgate'

const client = new Connection({
  createRecipe({ build }) { return {/*...*/} }
})

Recipe API

createRecipe is a higher-order function for transforming a request's parameters and response. It is a useful tool for morphing the LCU's API into your own. There are several ways to use the functions provided by the callback, and we'll take a look at each one.

Intro

Step 1: Create a recipe

This is identical to the builder API, except the request function isn't built until a hexgate instance is given to the recipe. This is useful for modeling requests ahead of time for usage in other places.

import { createRecipe } from "hexgate"

/**
 * <T extends HttpsClient>(httpsClient: T) => 
 *   (arg: string[], init?: RequestInit) => 
 *     Promise<ApiResponse<{...}>>
 */
const getSummonersFromNamesRecipe = createRecipe(({ build }) =>
  build('/lol-summoner/v2/summoners/names')
    .method('post')
    .create()
)

Step 2: Once you have a recipe, you just need to pass it a Hexgate.

const getSummonersFromNames = getSummonersFromNamesRecipe(httpsClient)

const summoners = await getSummonersFromNames(['dubbleignite'])
console.table(summoners.data)

🦋 Transforming requests

Use wrap, from, to, and unwrap to design your api.

const summonersRecipe = createRecipe(({ build, wrap, from, to, unwrap }) => ({
  getSummoners: {
    /**
     * Default for reference.
     * (arg: { ids?: string; }, init?: RequestInit) => Promise<ApiResponse<{...}>>
     */
    v2SummonersDefault: build('/lol-summoner/v2/summoners')
      .method('get')
      .create(),

    /**
     * unwrap extracts the data property from an ApiResponse.
     * (arg: { ids?: string }, init?: RequestInit) => Promise<{...}>
     */
    v2SummonersAwaited: unwrap(
      build('/lol-summoner/v2/summoners').method('get').create(),
    ),

    /**
     * wrap let's us overwrite the parameters type by supplying conversion functions.
     * (summonerIds: (number | `${number}`)[], init?: RequestInit | undefined) => Promise<{...}>
     */
    fromSummonerIds: wrap(
      build('/lol-summoner/v2/summoners').method('get').create(),
    )({
      // The return type of `from` is constrained by the expected return type of the function being wrapped.
      from(summonerIds: Array<`${number}` | number>, init?) {
        return [{ ids: JSON.stringify(summonerIds) }, init];
      },
      // awaits data similarly to `unwrap`
      to,
    }),
  },
}));

⚒️ Recipe, RecipeApiFn, and CreateWithRecipe

Some features have options that accept a Recipe, the product of createRecipe, or a RecipeApiFn, the api argument expected by createRecipe. You can achieve similar functionality in your own code by extending CreateWithRecipe or implementing its overloaded constructor signature.

import type { CreateWithRecipe } from 'hexgate'

class Foo<T> extends CreateWithRecipe<T> {}
new Foo(recipe)
new Foo((recipeApi) => "your recipe" as const)

Exporting recipes

If you want to export a recipe, you might get a type error. This is because the return type of createRecipe is inferred with references to @cuppachino/openapi-fetch and node-fetch-commonjs. To fix this, install the packages as dev dependencies and apply one of the following solutions to your tsconfig.json:

Map paths (Recommended)

Use this option if you are making a library.

{
  "compilerOptions": {
    "paths": {
      "@cuppachino/openapi-fetch": ["./node_modules/@cuppachino/openapi-fetch"],
      "node-fetch-commonjs": ["./node_modules/node-fetch-commonjs"]
    }
  }
}

Add types to the global scope (apps)

This can be used in applications, but it's not recommended.

{
  "compilerOptions": {
    "types": ["@cuppachino/openapi-fetch", "node-fetch-commonjs"]
  }
}

Additional features

LcuValue

The LcuValue class implements Update and CreateWithRecipe. It's useful for caching data retrieved from the LCU.

import { Connection, LcuValue, type OperationResponses } from 'hexgate'

type LolOwnedChampionsMinimal =
  OperationResponses<'GetLolChampionsV1OwnedChampionsMinimal'>

class ChampionLookup extends LcuValue<LolOwnedChampionsMinimal> {
  constructor() {
    super(({ build, unwrap }) =>
      unwrap(
        build('/lol-champions/v1/owned-champions-minimal')
          .method('get')
          .create()
      )
    )
  }

  championById(id: string | number | undefined) {
    return this.inner?.find((c) => c.id === Number(id ?? 0))
  }
}

const champions = new ChampionLookup()

const client = new Connection({
  async onConnect(con) {
    await champions.update(con.https)
    con.logger.info(
    champions.championById(1) satisfies
        | Partial<LolOwnedChampionsMinimal>[number]
    )
  }
})

client.connect()

Development

This package uses pnpm to manage dependencies. If you don't have pnpm, it can be installed globally using npm, yarn, brew, or scoop, as well as some other options. Check out the pnpm documentation for more information.

pnpm i