Charm + ReadonlyMap Issue: forEach shows data but get() returns nil for same userId in atom sub

I'm having a weird issue with Charm in my RBXTS project on Roblox. I'm using an atom that holds a ReadonlyMap<number, PlayerData> for player states (where keys are UserIds as numbers). When a player joins, they're added to the map during hydration (I'm using charm-sync + tether), and I can see their initial state on the client-side (e.g., afk: false). After about 2 seconds, I update the local player's afk to true by creating a new map immutably and setting the atom. In my subscribe callback, I iterate over the state with forEach, and it correctly prints all entries, including my local UserId with the updated afk: true. However, in the same callback, when I manually do state.get(localPlayer.UserId), it returns nil every time; even though the UserId matches exactly what's shown in the iteration. Here's the relevant code:
import { atom, subscribe } from "@rbxts/charm";
import { Players } from "@rbxts/services";
import { playerState } from "shared/core/state"; // Atom<ReadonlyMap<number, PlayerData>>

// Subscription
subscribe(playerState, (state) => {
state.forEach((data, key) => {
print(`Key: ${key}, afk: ${data.afk}`); // myUserId -> actual data as expected
});

print(`manual fetch: ${state.get(Players.LocalPlayer.UserId)?.afk}`); // .get(myUserId) -> nil
});
import { atom, subscribe } from "@rbxts/charm";
import { Players } from "@rbxts/services";
import { playerState } from "shared/core/state"; // Atom<ReadonlyMap<number, PlayerData>>

// Subscription
subscribe(playerState, (state) => {
state.forEach((data, key) => {
print(`Key: ${key}, afk: ${data.afk}`); // myUserId -> actual data as expected
});

print(`manual fetch: ${state.get(Players.LocalPlayer.UserId)?.afk}`); // .get(myUserId) -> nil
});
Output (before update):
Iteration prints: "Key: 4723581343, afk: false" (that's my UserId)
Manual fetch: nil
Iteration prints: "Key: 4723581343, afk: false" (that's my UserId)
Manual fetch: nil
Output (after update):
Iteration prints: "Key: 4723581343, afk: true" (that's my UserId)
Manual fetch: nil
Iteration prints: "Key: 4723581343, afk: true" (that's my UserId)
Manual fetch: nil
The playerState is updated immutably like this:
export function alterPlayerState<NewState extends PlayerData>(
userId: number,
alterState: (oldPlayerState: PlayerData) => NewState,
) {
const currentPlayerState = playerState();
const playerData = currentPlayerState.get(userId);

playerState(Remap.set(currentPlayerState, userId, alterState(playerData ?? DEFAULT_PLAYER_DATA)));
}

// somewhere else
alterPlayerState(player.UserId, (oldPlayerState) => ({
...oldPlayerState,
afk: oppositeState,
}));
export function alterPlayerState<NewState extends PlayerData>(
userId: number,
alterState: (oldPlayerState: PlayerData) => NewState,
) {
const currentPlayerState = playerState();
const playerData = currentPlayerState.get(userId);

playerState(Remap.set(currentPlayerState, userId, alterState(playerData ?? DEFAULT_PLAYER_DATA)));
}

// somewhere else
alterPlayerState(player.UserId, (oldPlayerState) => ({
...oldPlayerState,
afk: oppositeState,
}));
This is definitely being dispatched to the client, hence I made a debug message for incoming updates from the client syncer:
{
["data"] = ▼ {
["playerState"] = ▼ {
["4723581343"] = ▼ {
["afk"] = true
}
}
},
["type"] = "patch"
}
{
["data"] = ▼ {
["playerState"] = ▼ {
["4723581343"] = ▼ {
["afk"] = true
}
}
},
["type"] = "patch"
}
LocalPlayer is fully loaded (this is post-join), and UserId is a number (no string conversion issues that I can see). Iteration works fine, so the entry exists, but get() on ReadOnlyMap<K,V> fails. Is this a bug with ReadonlyMap, Charm's atom behavior, or something in rbxts? Any ideas?
11 Replies
Tverksaac 2.0
Tverksaac 2.04mo ago
you manually fetch data on client or on server?
Lypt1x
Lypt1xOP4mo ago
Right after the server sent the client the patched atom, on the client, I'm fetching the userId from the Map in the subscription callback. Basically playerState changes on Server <=> synchronization <=> on client: subscribe(playerState, (map) => { .. }) and when the subscribe callback is called, it shows me the updated map, but I can't invoke get(userId) with the same userId I also iterated on
Tverksaac 2.0
Tverksaac 2.04mo ago
can you give the code files?
Lypt1x
Lypt1xOP4mo ago
hey, sorry for no response, because I haven't had the time to be active for few days. Recently, I just gone through some checks, and found out, that my atom has probably changed its type during runtime?
No description
Lypt1x
Lypt1xOP4mo ago
this is so strange I assume, the serializer turns every number into a string, and my syncer on the client-side is turning my Atom<ReadonlyMap<number, PlayerData>> into a Atom<ReadonlyMap<string, PlayerData>> during runtime.
Lypt1x
Lypt1xOP4mo ago
here's how I'm syncing the client
No description
No description
runic
runic4mo ago
try using the debug middleware please also show your message types i want to rule out tether/serio being the issue this seems good to me oh yeah this is 100% the issue not sure if this is a user error on my part or this is a flamework issue
runic
runic4mo ago
yeah cause this was an issue i was dealing with in the past while making tether
No description
No description
runic
runic4mo ago
@Fireboltofdeath i hate this was there a reason it does this cause i cant remember
Unknown User
Unknown User4mo ago
Message Not Public
Sign In & Join Server To View
runic
runic4mo ago
so evilllllllllllllllllllllllllllllllllll

Did you find this page helpful?