decentraland / decentraland/sdk
[BUG] isStateSyncronized() never returns true with local auth-server / hammurabi-server (state-request retry silently disabled)
- Dominant language
- No language data
- Stars
- 4
- Forks
- 5
- PR merge metrics
- No merged PRs in 30d
Description
### **Issue Description:**
On the `js-sdk-toolchain` `auth-server` branch (experimental authoritative-multiplayer support), `isStateSyncronized()` (from `@dcl/sdk/network`) sometimes never flips to `true` during local scene development against a local `hammurabi-server` (`@dcl/hammurabi-server`, repo `decentraland/hammurabi-headless`) instance. No errors appear in either the client console or the Hammurabi/server console. A trivial code change (whitespace/line-break + save, triggering a scene hot-reload) sometimes unblocks it; other times a full Creator Hub exit + restart is required.
**Root cause found in `packages/@dcl/sdk/src/network/message-bus-sync.ts` (`auth-server` branch):**
- The `REQ_CRDT_STATE` handler responds to *any* peer that asks for state, not just the authoritative server — there is no `isServerAtom` guard on it:
```ts
binaryMessageBus.on(CommsMessage.REQ_CRDT_STATE, async (data, sender) => {
const chunks = engineToCrdt(engine)
// ... emits RES_CRDT_STATE back to `sender`, regardless of whether *this* peer is the server
})
```
- The `RES_CRDT_STATE` handler unconditionally clears the "waiting for state" flag *before* checking whether the response actually came from the authoritative server:
```ts
binaryMessageBus.on(CommsMessage.RES_CRDT_STATE, async (data, sender) => {
requestingState = false
elapsedTimeSinceRequest = 0
if (isServerAtom.getOrNull() || sender !== AUTH_SERVER_PEER_ID) return // <-- checked AFTER requestingState is already cleared
...
stateIsSyncronized = true
})
```
- The only thing that re-triggers a state request is the retry system, and it explicitly requires `requestingState` to still be `true`:
```ts
engine.addSystem((dt) => {
if (requestingState && !stateIsSyncronized) { /* retry requestState() every 2s */ }
})
```
Put together: in any local scene with more than one connected peer (e.g. two Explorer/CH instances, or any regular client that isn't the authoritative server), a `RES_CRDT_STATE` reply from a *non-server* peer arriving before/instead of the real Hammurabi response will silently set `requestingState = false` and return early. Since `stateIsSyncronized` was never set to `true`, and `requestingState` is now `false`, the retry system's guard condition (`requestingState && !stateIsSyncronized`) never fires again — the client is now permanently stuck waiting with no further requests, no errors, and no logs (all diagnostic logging in this file is gated behind `DEBUG_NETWORK_MESSAGES`, which defaults to `false`).
This matches all reported symptoms:
- No client/server errors — the "stuck" state is a silently-disabled retry loop, not a crash.
- Fixed by a trivial hot-reload — reloading the scene re-runs `addSyncTransport()`, re-creating `requestingState`/`stateIsSyncronized` and re-triggering the initial request via `RealmInfo.onChange`/`players.onEnterScene`.
- Sometimes requires a full Creator Hub restart — if the hang is instead in the Hammurabi process/room-join itself (or hot-reload doesn't re-trigger a fresh `RealmInfo` transition), only killing and respawning the whole local dev server process resolves it.
Git history on this file shows this retry/empty-state logic has already been patched twice for adjacent bugs (`Fix empty CRDT state response causing infinite retries` #1342, `fix request state if not received`), suggesting this area is known to be fragile but this specific ordering bug (clearing `requestingState` before the sender check) hasn't been addressed yet.
### **SDK:**
- [ ] SDK6
- [x] SDK7
### **Tool:**
- [ ] Editor
- [x] CLI
CLI Version: `js-sdk-toolchain` `auth-server` branch (experimental authoritative-server support)
Node Version: N/A
### **Steps to reproduce:**
1. Run a scene locally on the `auth-server` branch with a locally-spawned `hammurabi-server` (via Creator Hub / `sdk-commands start`) acting as the authoritative server.
2. Have more than one client/peer connect to the same local scene room (or otherwise cause a `RES_CRDT_STATE` message to arrive from a peer other than the authoritative server).
3. Poll `isStateSyncronized()` from scene code — it can remain `false` indefinitely with no console errors.
4. Save a trivial whitespace change to trigger a hot reload — sync sometimes recovers; otherwise a full Creator Hub restart is needed.
### **Expected behaviour:**
`isStateSyncronized()` should reliably become `true` once the authoritative server responds with the CRDT state, and the client should keep retrying `REQ_CRDT_STATE` until it does — regardless of whether other non-server peers also reply to the state request.
### **Current behaviour:**
A `RES_CRDT_STATE` reply from a non-authoritative-server peer disables the retry loop before the sender is validated, so the client can get permanently stuck never receiving/accepting the real state, with no errors surfaced anywhere.
### **Reproduction rate:**
Intermittent — reported as "sometimes" by the user; likely correlates with the number of connected peers / timing of the local Hammurabi server joining the room relative to other peers.
### **Code Snippets:**
See root cause section above — `packages/@dcl/sdk/src/network/message-bus-sync.ts`, `RES_CRDT_STATE` and `REQ_CRDT_STATE` handlers, and the retry `engine.addSystem`.
### **Platforms:**
- [x] Not platform specific
### **Browser:**
- [x] Not Browser Specific
### **Environment:**
- [x] local preview
### **Evidence:**
No client/server console errors were observed by the reporter — the bug produces silent state, not an exception. Enabling `globalThis.DEBUG_NETWORK_MESSAGES = true` in the scene should surface `[REQ_CRDT_STATE]` / `[Processing CRDT State]` logs from the affected handlers and confirm whether a non-server `RES_CRDT_STATE` is arriving first.
### **Additional Notes:**
Suggested fix direction: only clear `requestingState`/`elapsedTimeSinceRequest` inside the `RES_CRDT_STATE` handler once the sender has been validated as `AUTH_SERVER_PEER_ID` (i.e. move those two lines below the early-return check), so a stray response from a non-server peer can't disable the retry loop.
Other possible contributing factors worth ruling out while diagnosing (not confirmed root cause, but relevant context):
- Hammurabi's own scene execution runs sandboxed per-scene inside a QuickJS VM with per-turn interrupt limits; a stalled scene turn there fails silently into the host process's stdout rather than a visible UI console.
- Local dev spawns `@dcl/hammurabi-server` via the mutable `next` dist-tag, so behavior can vary between sessions/machines pulling different Hammurabi builds against the same scene bundle.
No existing open issue in `decentraland/sdk`, `decentraland/hammurabi-headless`, or `decentraland/js-sdk-toolchain` was found covering this specific symptom.
---
Reported by Stom
Requested by Gabriel Díaz via Slack
Contributor guide
Assessment
This issue has not been assessed yet.