TypeScript Module Error: JavaScript functions cannot be inline

I am writing my server side code for my game in TypeScript, and built a base Game class with extending mini-games. Inside of each Game or mini game class is all the functions needed for registerMatch. When I attempt to run my code, I get this error:

{"level":"fatal","ts":"2022-11-30T01:31:18.121Z","caller":"main.go:158","msg":"Failed initializing runtime modules","error":"GoError: js match handler \"matchInit\" function for module \"ballgame\" global id could not be extracted: function literal found: javascript functions cannot be inlined\n\tat github.com/heroiclabs/nakama/v3/server.(*RuntimeJavascriptInitModule).registerMatch.func1 (native)\n\tat InitModule (index.js:407:26(39))\n\tat native\n"}

I am running Nakama 3.14 and am using TypeScript/JavaScript.

My code is setup as such:
Main.ts

import { Game } from './games'
import { BallGame } from './games/ball'

const InitModule = ... {
    const game: Game = new BallGame()
    initializer.registerMatch(game.id, {
        matchInit: game.matchInit,
        matchJoinAttempt: game.matchJoinAttempt,
        matchJoin: game.matchJoin,
        matchLeave: game.matchLeave,
        matchLoop: game.matchLoop,
        matchSignal: game.matchSignal,
        matchTerminate: game.matchTerminate,
    })
}

Games.ts

export abstract class Game {
    id: string
    ...

    matchInit(
        ctx: nkruntime.Context,
        logger: nkruntime.Logger,
        nk: nkruntime.Nakama,
        params: { [key: string]: string }
    ): { state: GameState; tickRate: number; label: string } {
        ...
        return {
            state: { ... },
            tickRate: this.tickRate,
            label: gameInviteCode,
        }
    }

    // ... (all other functions needed for registerMatch)
}

BallGame.ts

export class BallGame extends Game {
    id = 'ball-game'
    ...
    matchInit(
        ctx: nkruntime.Context,
        logger: nkruntime.Logger,
        nk: nkruntime.Nakama,
        params: { [key: string]: string }
    ): { state: BallGameState; tickRate: number; label: string } {
        const { state, tickRate, label } = super.matchInit(ctx, logger, nk, params)
        ...
        return {
            state: {
                ...state,
                ...
            },
            tickRate,
            label,
        }
    }
}

I see that this error is referencing me using functions that then get compiled to be inline, but is there anything I can do here to keep my functionality the same (ie using classes of some sort and hopefully calling super)? I tried converting to arrow functions without success (may have been doing it wrong).

Thanks for any help you may have.

Hello @hcopp, unfortunately there are some limitations on the JS engine and all the functions that are registered need to be at the top level scope. I believe that classes can be referenced within those functions, but not the other way around. Using classes anywhere else should work too.

You need to make sure that in the transpiled code the functions are not inlined but referenced by a top level identifier within the registration functions.

Hope this helps.

1 Like

Hi @sesposito,

I am commenting here to not open a new ticket for the same issue.

I have the same error about inlined functions but I do believe that my functions are already top-level.

I did the same logic where I have a match_handler class that implements from nkruntime.MatchHandler. Then, I have multiple sub match_handler classes that implements from match_handler, such as five_hand_poker_match_handler.

Here is a snippet from each classes:

interface MatchLabel {
  //CasiNoya Labels
  training_five_hand_poker: number;
  training_blackjack: number;
  training_chinese_poker: number;
  training_war: number;

  normal_five_hand_poker: number;
  normal_blackjack: number;
  normal_chinese_poker: number;
  normal_war: number;

  rank_five_hand_poker: number;
  rank_blackjack: number;
  rank_chinese_poker: number;
  rank_war: number;
}

interface State {
  // The winner of the current game.
  // The winner positions.

  // FOR CASINOYA
  // Match label
  label: MatchLabel;
  // Ticks where no actions have occurred.
  emptyTicks: number;
  // Currently connected users, or reserved spaces.
  presences: { [userId: string]: nkruntime.Presence | null };
  // Number of users currently in the process of connecting to the match.
  joinsInProgress: number;
  // True if there's a game currently in progress.
  playing: boolean;
  // Current state of the game.
  game_state: GameState;
  // Ticks until they must submit their move.
  deadlineRemainingTicks: number;
  // Ticks until the next game starts, if applicable.
  nextGameRemainingTicks: number;
  // AI playing mode
  ai: boolean;
  // A move message from AI player
  aiMessage: nkruntime.MatchMessage | null;
}

abstract class Match_Handler implements nkruntime.MatchHandler<State> {
  protected moduleName: string;
  protected tickRate: number;
  protected maxEmptySec: number;
  protected delaybetweenGamesSec: number;
  protected turnTimeFastSec: number;
  protected turnTimeNormalSec: number;

  constructor(
    $moduleName: string,
    $tickRate: number,
    $maxEmptySec: number,
    $delaybetweenGamesSec: number,
    $turnTimeFastSec: number,
    $turnTimeNormalSec: number
  ) {
    this.moduleName = $moduleName;
    this.tickRate = $tickRate;
    this.maxEmptySec = $maxEmptySec;
    this.delaybetweenGamesSec = $delaybetweenGamesSec;
    this.turnTimeFastSec = $turnTimeFastSec;
    this.turnTimeNormalSec = $turnTimeNormalSec;
  }

  abstract matchInit(
    ctx: nkruntime.Context,
    logger: nkruntime.Logger,
    nk: nkruntime.Nakama,
    params: { [key: string]: string }
  ): { state: State; tickRate: number; label: string };
class Five_Hand_Poker_Match_Handler extends Match_Handler {
  private static _instance_normal?: Five_Hand_Poker_Match_Handler;
  private static _instance_training?: Five_Hand_Poker_Match_Handler;

  private constructor(
    $moduleName: string,
    $tickRate: number,
    $maxEmptySec: number,
    $delaybetweenGamesSec: number,
    $turnTimeFastSec: number,
    $turnTimeNormalSec: number
  ) {
    super(
      $moduleName,
      $tickRate,
      $maxEmptySec,
      $delaybetweenGamesSec,
      $turnTimeFastSec,
      $turnTimeNormalSec
    );
  }

  matchInit(
    ctx: nkruntime.Context,
    logger: nkruntime.Logger,
    nk: nkruntime.Nakama,
    params: { [key: string]: string }
  ) {
    let label: MatchLabel;
    if (this.$moduleName == "five_hand_poker_training") {
      label = {
        //CasiNoya Labels
        training_five_hand_poker: 1,
        training_blackjack: 0,
        training_chinese_poker: 0,
        training_war: 0,

        normal_five_hand_poker: 0,
        normal_blackjack: 0,
        normal_chinese_poker: 0,
        normal_war: 0,

        rank_five_hand_poker: 0,
        rank_blackjack: 0,
        rank_chinese_poker: 0,
        rank_war: 0,
      };
    } else {
      label = {
        //CasiNoya Labels
        training_five_hand_poker: 0,
        training_blackjack: 0,
        training_chinese_poker: 0,
        training_war: 0,

        normal_five_hand_poker: 1,
        normal_blackjack: 0,
        normal_chinese_poker: 0,
        normal_war: 0,

        rank_five_hand_poker: 0,
        rank_blackjack: 0,
        rank_chinese_poker: 0,
        rank_war: 0,
      };
    }

    let state: State = {
      // The winner of the current game.
      // The winner positions.

      // FOR CASINOYA
      // Match label
      label: label,
      // Ticks where no actions have occurred.
      emptyTicks: 0,
      // Currently connected users, or reserved spaces.
      presences: {},
      // Number of users currently in the process of connecting to the match.
      joinsInProgress: 0,
      // True if there's a game currently in progress.
      playing: false,
      // Current state of the game.
      game_state: GameState.LOBBY,
      // Ticks until they must submit their move.
      deadlineRemainingTicks: 0,
      // Ticks until the next game starts, if applicable.
      nextGameRemainingTicks: 0,
      // AI playing mode
      ai: false,
      // A move message from AI player
      aiMessage: null,
    };
    return {
      state: state,
      tickRate: this.tickRate, // 1 tick per second = 1 MatchLoop func invocations per second
      label: JSON.stringify(label),
    };
  }

Am I wrong to assume that I already have top-level functions? If yes, can you provide some indications on how to perform the desired tasks of having multiple modules for multiple different game modes?
Thanks!

I’m not entirely sure how class types get transpiled to ES5 compliant JS code, but if the matchInit function is declared inside a class I don’t think it’ll be globally scoped at the top level.

Thank you, I got it to work. I figured out a way to declare the functions in variables then have different versions of the functions depending on the game mode!