How can I make this Profile Service script be more efficient? (ROBLOX TYPESCRIPT)

So, I’m trying to rewrite my game to typescript. I’ve rewritten my datastore script. It works great but I’m wondering if there’s anything I could do better. This is my code:

import ProfileService from "@rbxts/profileservice";
import { Players } from "@rbxts/services";

const DataManager = {
  get: function (player: Player) {
    return new Promise(function (resolve, reject) {
      if (profiles[player.Name] === undefined) {
        reject("User doesn't have a profile");
      } else {
        resolve(profiles[player.Name]);
      }
    });
  },
};

const defaultData = {
  Points: 20,
  Wins: 0,
};

const profiles: ProfilesArray = [];
const ProfileStore = ProfileService.GetProfileStore("StatsStore", defaultData);

async function onPlayerAdded(player: Player) {
  function onRelease() {
    profiles[player.Name] = undefined;
    player.Kick();
  }

  const profile = ProfileStore.LoadProfileAsync(`Player_${player.UserId}`);

  if (profile) {
    profile.AddUserId(player.UserId);
    profile.Reconcile();
    profile.ListenToRelease(onRelease);

    if (player.IsDescendantOf(Players)) {
      const leaderstats = player.WaitForChild("leaderstats");
      const winsValue = leaderstats.WaitForChild("Wins") as IntValue;
      const pointsValue = leaderstats.WaitForChild("Points") as IntValue;

      winsValue.Value = profile.Data.Wins;
      pointsValue.Value = profile.Data.Points;

      profiles[player.Name] = profile;
    } else {
      profile.Release();
    }
  }
}

async function onPlayerRemoving(player: Player) {
  try {
    const leaderstats = player.WaitForChild("leaderstats");
    const points = leaderstats.WaitForChild("Points") as IntValue;
    const wins = leaderstats.WaitForChild("Wins") as IntValue;

    const profile: any = await DataManager.get(player);

    profile.Data = {
      Points: points.Value,
      Wins: wins.Value,
    };

    profile.Release();
  } catch (e) {
    warn(`Data warning: ${e}`);
  }
}

Players.PlayerAdded.Connect(onPlayerAdded);
Players.PlayerRemoving.Connect(onPlayerRemoving);

export default DataManager;

If you have any questions feel free to ask! Any help is appreciated!

3 Likes

If you made sure you did not have any typing errors or values of type any, then you did a good job.

However, you may be asking the wrong question. The main reasons for using TypeScript in roblox development is its robust and mature typing system and its valuable refactoring tools. By using their typing system, what you are doing is sacrificing efficiency for more safety. This is because Roblox-ts needs a runtime that makes TypeScript as a language and its typing system compatible with the engine. Remember that Lua and JavaScript (TypeScript without typing) are different languages with different goals. Something as subtle as indexing arrays from 0 or 1 can lead to different algorithms/systems. On the other hand, TypeScript is a more abstract language than Lua, which means that you are further away from the engine (i.e. in a more abstract context) and will not be able to take full advantage of the engine’s capabilities. Again it seems subtle, but games are such complex things that they inevitably end up being noticed.

So when you develop with TypeScript you are providing security to your game (it won’t break so unexpectedly), working at a more abstract and easier to understand level and with valuable tools built into the language. But if you are more interested in efficiency, then you should work with Lua.

1 Like

Hi there @TheH0meLands , I’m a huge Typescript fan, I’ve been a professional developer for 27 years, and I have some notes from peer reviewing your script.

  1. profiles was declared to be an array but the player’s name is being used as a key. In that kind of situation I would usually use a type of Map.

  2. Instead of the player’s name, let’s use the UserId as a key. It’s immutable and is more efficient to index.

  3. Let’s make a class for the DataManager, which seems to just have the one method now, we’ll make it more of a manager of the in-game player’s profiles. The structure you have now for DataManager isn’t a class, it’s just an object.

  4. No async work is done so we can remove the async markers and the Promise usages. The naming convention in Roblox is “if it ends with Async it takes a long time” so we need to manage any thread that calls such a function. There’s only one on this page and it’s ProfileStore.LoadProfileAsync. Our call to that is already in a managed thread because it will be executed by an event handler (PlayerAdded).

  5. Let’s define a type of IPlayerData for the shape of your profile Data. This will give you safety if you make modifications to the profile data in your code, it will be an error if you try to set an unknown key.

  6. We’ll need to add a loop for the current players of the game, in addition to the PlayerAdded handler. When your script first starts to run, there will already be 1 player in the game, and the PlayerAdded handler might not fire for them.

Here’s my rewrite that takes the above into consideration:

import ProfileService from "@rbxts/profileservice";
import { Profile } from "@rbxts/profileservice/globals";
import { Players } from "@rbxts/services";

interface IPlayerData {
    Points: number;
    Wins: number;
}
type ProfileStorageType = Profile<IPlayerData>;

const defaultData = {
  Points: 20,
  Wins: 0,
};

class DataManagerClass {
    private _profiles = new Map<number, ProfileStorageType>;
    addProfile(UserId: number, profile: ProfileStorageType) {
        this._profiles.set(UserId, profile);
    }
    removeProfile(UserId: number) {
        this._profiles.delete(UserId);
    }
    getProfile(UserId: number): ProfileStorageType | undefined {
        return this._profiles.get(UserId);
    }
}

const DataManager = new DataManagerClass();
const ProfileStore = ProfileService.GetProfileStore("StatsStore", defaultData);

function onPlayerAdded(player: Player) {
  function onRelease() {
    DataManager.removeProfile(player.UserId);
    player.Kick();
  }

  const profile = ProfileStore.LoadProfileAsync(`Player_${player.UserId}`);

  if (profile) {
    profile.AddUserId(player.UserId);
    profile.Reconcile();
    profile.ListenToRelease(onRelease);

    if (player.IsDescendantOf(Players)) {
      const leaderstats = player.WaitForChild("leaderstats");
      const winsValue = leaderstats.WaitForChild("Wins") as IntValue;
      const pointsValue = leaderstats.WaitForChild("Points") as IntValue;

      const playerData: IPlayerData = profile.Data;

      winsValue.Value = playerData.Wins;
      pointsValue.Value = playerData.Points;

      DataManager.addProfile(player.UserId, profile);
    } else {
      profile.Release();
    }
  }
}

function onPlayerRemoving(player: Player) {
  try {
    const leaderstats = player.WaitForChild("leaderstats");
    const points = leaderstats.WaitForChild("Points") as IntValue;
    const wins = leaderstats.WaitForChild("Wins") as IntValue;

    const profile = DataManager.getProfile(player.UserId);
    if (profile) {
        const playerData: IPlayerData = profile.Data;
        playerData.Points = points.Value;
        playerData.Wins = wins.Value;
    
        profile.Release();
    } else {
        warn('profile not found during PlayerRemoving', player.UserId)
    }
  } catch (e) {
    warn(`Data warning: ${e}`);
  }
}

// for players currently in the game
Players.GetPlayers().forEach(player => onPlayerAdded(player));
// for players who join in the future
Players.PlayerAdded.Connect(onPlayerAdded);
Players.PlayerRemoving.Connect(onPlayerRemoving);

export default DataManager;

I enjoy mentoring other developers in Typescript, and I’m available to the community to help with any Typescript questions. My game, Slither Simulator is around 19,000 lines of Typescript code and has a custom Replication system.

5 Likes

would this be a .server.ts or just .ts because for me the functions (playeradded,playerremoving) dont work as its a module script if its .ts, but if it isnt .ts i cant export datamanager

ok so the version this guy uses is an older version where you have to do export default in order for it to compile. Newer versions dont need this. His script is .server.ts iirc

You’re right, this should be multiple files. You would not want to export from the same file where you’re setting up the PlayerAdded/PlayerRemoved event handlers. You’re right that you would probably have a .server.ts file where your PlayerAdded/PlayerRemoved and other event callbacks are set up. Then your ProfileServiceWrapper would be in a separate module script where it could be exported, and then imported by other scripts.

1 Like