From 195df07a37a9ae61136b233baba9f47e7d87b0c0 Mon Sep 17 00:00:00 2001 From: nicollassilva Date: Mon, 27 Jul 2026 08:21:23 -0300 Subject: [PATCH 1/5] fix: CloseButton import error --- .../nitro-react/src/components/CloseButton.tsx | 16 ++++++++-------- .../object-infostand/InfostandFurniView.tsx | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/nitro-react/src/components/CloseButton.tsx b/packages/nitro-react/src/components/CloseButton.tsx index d4d12c6..9f3b6e7 100644 --- a/packages/nitro-react/src/components/CloseButton.tsx +++ b/packages/nitro-react/src/components/CloseButton.tsx @@ -2,7 +2,7 @@ import { forwardRef, type HTMLAttributes } from 'react'; import { cn, cva, type VariantProps } from '#base/utils'; -const closebuttonVariantsConfig = { +const closeButtonVariantsConfig = { variant: { // default '0': '[border-image-source:var(--closebutton-0-default-src)] [border-image-slice:0_0_0_0_fill] [border-image-width:0px_0px_0px_0px] [border-image-repeat:stretch] hover:[border-image-source:var(--closebutton-0-hovering-src)] hover:[border-image-slice:0_0_0_0_fill] hover:[border-image-width:0px_0px_0px_0px] hover:[border-image-repeat:stretch] active:[border-image-source:var(--closebutton-0-pressed-src)] active:[border-image-slice:0_0_0_0_fill] active:[border-image-width:0px_0px_0px_0px] active:[border-image-repeat:stretch]', @@ -17,30 +17,30 @@ const closebuttonVariantsConfig = { }, } as const; -const closebuttonVariants = cva( +const closeButtonVariants = cva( '', { - variants: closebuttonVariantsConfig, + variants: closeButtonVariantsConfig, defaultVariants: { variant: '0', }, } ); -type ClosebuttonVariantProps = VariantProps; +type CloseButtonVariantProps = VariantProps; -interface ClosebuttonProps extends HTMLAttributes, ClosebuttonVariantProps { +interface CloseButtonProps extends HTMLAttributes, CloseButtonVariantProps { className?: string; } -export const Closebutton = forwardRef( +export const CloseButton = forwardRef( ({ className, variant, ...props }, ref) => (
) ); -Closebutton.displayName = 'Closebutton'; +CloseButton.displayName = 'CloseButton'; diff --git a/packages/nitro-react/src/views/room-widgets/object-infostand/InfostandFurniView.tsx b/packages/nitro-react/src/views/room-widgets/object-infostand/InfostandFurniView.tsx index b3f7c52..eec38fb 100644 --- a/packages/nitro-react/src/views/room-widgets/object-infostand/InfostandFurniView.tsx +++ b/packages/nitro-react/src/views/room-widgets/object-infostand/InfostandFurniView.tsx @@ -1,7 +1,7 @@ import { FurniturePickupMode, IRoomFurnitureData } from "@nitrodevco/nitro-api"; import { Border, Button } from "#base/components"; -import { CloseButton } from "#base/components/Closebutton"; +import { CloseButton } from "#base/components/CloseButton"; import { FurnitureImage } from "#base/components/FurnitureImage"; import { useLocalizationStore } from "#base/stores"; From b59fd7e07207cdbdf23bf98c421240d363d422c1 Mon Sep 17 00:00:00 2001 From: nicollassilva Date: Tue, 28 Jul 2026 15:42:12 -0300 Subject: [PATCH 2/5] =?UTF-8?q?Fix=20O(n=C2=B2)=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../communication/WebSocketContextProvider.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx b/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx index 8a35e72..03a0ad5 100644 --- a/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx +++ b/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx @@ -155,18 +155,22 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { const reader = new BinaryReader(wsBuffer.current); - while (wsBuffer.current.byteLength) { - if (wsBuffer.current.byteLength < 4) break; + let consumed = 0; + while (wsBuffer.current.byteLength - consumed >= 4) { const length = reader.readInt(); - if (length > (wsBuffer.current.byteLength - 4)) break; + if (length > (wsBuffer.current.byteLength - consumed - 4)) break; const extracted = reader.readBytes(length); wrappers.push(new EvaWireDataWrapper(extracted.readShort(), extracted)); - wsBuffer.current = wsBuffer.current.slice(length + 4); + consumed += length + 4; + } + + if (consumed) { + wsBuffer.current = wsBuffer.current.slice(consumed); } return wrappers; From fb038103406b4a7a0fa79df8239c2785cc1eb402 Mon Sep 17 00:00:00 2001 From: nicollassilva Date: Tue, 28 Jul 2026 15:55:34 -0300 Subject: [PATCH 3/5] Closing the connection upon receiving a malformed packet --- .../WebSocketContextProvider.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx b/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx index 03a0ad5..4ad6314 100644 --- a/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx +++ b/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx @@ -157,20 +157,25 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { let consumed = 0; - while (wsBuffer.current.byteLength - consumed >= 4) { - const length = reader.readInt(); - - if (length > (wsBuffer.current.byteLength - consumed - 4)) break; + try { + while (wsBuffer.current.byteLength - consumed >= 4) { + const length = reader.readInt(); - const extracted = reader.readBytes(length); + if (length < 2) { + ws.current?.close(1011, `WebSocket: Malformed packet length: ${length}`); + break; + } + + if(length > reader.remaining()) break; - wrappers.push(new EvaWireDataWrapper(extracted.readShort(), extracted)); + const extracted = reader.readBytes(length); - consumed += length + 4; - } + wrappers.push(new EvaWireDataWrapper(extracted.readShort(), extracted)); - if (consumed) { - wsBuffer.current = wsBuffer.current.slice(consumed); + consumed += length + 4; + } + } finally { + if (consumed) wsBuffer.current = wsBuffer.current.slice(consumed); } return wrappers; From c0448fce93c1466295978b4fdbd562523e336006 Mon Sep 17 00:00:00 2001 From: nicollassilva Date: Tue, 28 Jul 2026 16:16:23 -0300 Subject: [PATCH 4/5] Fixing websocket onclose --- .../communication/WebSocketContext.tsx | 1 + .../WebSocketContextProvider.tsx | 44 +++++++++++++++---- 2 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packages/nitro-react/src/context/communication/WebSocketContext.tsx b/packages/nitro-react/src/context/communication/WebSocketContext.tsx index e817fa3..5d49360 100644 --- a/packages/nitro-react/src/context/communication/WebSocketContext.tsx +++ b/packages/nitro-react/src/context/communication/WebSocketContext.tsx @@ -3,6 +3,7 @@ import { createContext } from 'react'; type WebSocketStore = { isAuthenticated: boolean, + isDisconnected: boolean, connect: () => void, send: (...packets: IOutgoingPacket[]) => void, subscribe: (event: IncomingPacketConstructor, handler: (data: T) => void) => void; diff --git a/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx b/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx index 4ad6314..6a8777b 100644 --- a/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx +++ b/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx @@ -18,6 +18,7 @@ type ProviderProps = { export const WebSocketContextProvider = ({ children }: ProviderProps) => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [isReady, setIsReady] = useState(false); + const [isDisconnected, setIsDisconnected] = useState(false); const { incomingByHeader, incomingCtors, incomingHeaderByCtor, registerManyIncoming } = useCommunicationIncoming(); const { outgoingHeaderByComposerName, registerManyOutgoing } = useCommunicationOutgoing(); //const socketUrl = useConfigurationStore(x => x.config['socket.url'] as string) ?? undefined; @@ -27,6 +28,7 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { const listeners = useRef, Array<(data: object) => void>>>(new Map()); const pendingClientMessages = useRef[]>([]); const pendingServerMessages = useRef([]); + const hasConnected = useRef(false); const connect = () => { try { @@ -34,12 +36,17 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { const socketUrl = params.get('socketUrl') ?? ''; if (!socketUrl || !socketUrl.length || ws.current) return; + if (hasConnected.current) return; - ws.current = new WebSocket(socketUrl); + hasConnected.current = true; - ws.current.binaryType = 'arraybuffer'; + const socket = new WebSocket(socketUrl); - ws.current.onopen = (event: Event) => { + ws.current = socket; + + socket.binaryType = 'arraybuffer'; + + socket.onopen = () => { send(new ClientHelloComposer({ production: production, platform: 'WEB', @@ -56,17 +63,27 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { })); }; - ws.current.onerror = (event: Event) => { + socket.onerror = (event: Event) => { NitroLogger.error('WebSocket error:', event); }; - ws.current.onclose = (event: CloseEvent) => { + socket.onclose = (event: CloseEvent) => { NitroLogger.warn('WebSocket closed:', event.code, event.reason); + + if (ws.current !== socket) return; + + ws.current = undefined; + wsBuffer.current = new ArrayBuffer(0); + + pendingClientMessages.current = []; + pendingServerMessages.current = []; + setIsAuthenticated(false); setIsReady(false); + setIsDisconnected(true); }; - ws.current.onmessage = (event: MessageEvent) => { + socket.onmessage = (event: MessageEvent) => { const array = new Uint8Array(wsBuffer.current.byteLength + event.data.byteLength); array.set(new Uint8Array(wsBuffer.current), 0); @@ -162,11 +179,12 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { const length = reader.readInt(); if (length < 2) { - ws.current?.close(1011, `WebSocket: Malformed packet length: ${length}`); + NitroLogger.error(`WebSocket: Malformed packet length: ${length}`); + ws.current?.close(); break; } - if(length > reader.remaining()) break; + if (length > reader.remaining()) break; const extracted = reader.readBytes(length); @@ -298,6 +316,14 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { registerManyOutgoing(GetOutgoingPackets()); }, []); + useEffect(() => () => { + const socket = ws.current; + + ws.current = undefined; + + socket?.close(1000, 'Client shutting down'); + }, []); + useEffect(() => { if (isAuthenticated) return; @@ -307,7 +333,7 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { }, [isAuthenticated, subscribe]); return ( - + {children} ); From ec1f2c6d078f756cdbb19f05f54c7ec26a9642d8 Mon Sep 17 00:00:00 2001 From: nicollassilva Date: Wed, 29 Jul 2026 06:32:50 -0300 Subject: [PATCH 5/5] Fix: Stale closure (dead packet queue) --- .../WebSocketContextProvider.tsx | 92 ++++++++++--------- 1 file changed, 48 insertions(+), 44 deletions(-) diff --git a/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx b/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx index 6a8777b..f0d1fdd 100644 --- a/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx +++ b/packages/nitro-react/src/context/communication/WebSocketContextProvider.tsx @@ -15,10 +15,11 @@ type ProviderProps = { children: ReactNode; } +type ConnectionPhase = 'idle' | 'connecting' | 'authenticating' | 'awaitingHandlers' | 'ready' | 'closed'; + export const WebSocketContextProvider = ({ children }: ProviderProps) => { - const [isAuthenticated, setIsAuthenticated] = useState(false); - const [isReady, setIsReady] = useState(false); - const [isDisconnected, setIsDisconnected] = useState(false); + const phase = useRef('idle'); + const [renderedPhase, setRenderedPhase] = useState('idle'); const { incomingByHeader, incomingCtors, incomingHeaderByCtor, registerManyIncoming } = useCommunicationIncoming(); const { outgoingHeaderByComposerName, registerManyOutgoing } = useCommunicationOutgoing(); //const socketUrl = useConfigurationStore(x => x.config['socket.url'] as string) ?? undefined; @@ -46,7 +47,11 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { socket.binaryType = 'arraybuffer'; + setPhase('connecting'); + socket.onopen = () => { + setPhase('authenticating'); + send(new ClientHelloComposer({ production: production, platform: 'WEB', @@ -74,13 +79,11 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { ws.current = undefined; wsBuffer.current = new ArrayBuffer(0); - + pendingClientMessages.current = []; pendingServerMessages.current = []; - setIsAuthenticated(false); - setIsReady(false); - setIsDisconnected(true); + setPhase('closed'); }; socket.onmessage = (event: MessageEvent) => { @@ -102,15 +105,7 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { try { if (wsBuffer.current.byteLength === 0) return; - const wrappers = decodeWrappers(); - - if (isAuthenticated && !isReady) { - pendingServerMessages.current.push(...wrappers); - - return; - } - - processWrappers(...wrappers); + dispatchWrappers(decodeWrappers()); } catch (err) { NitroLogger.error(err); } @@ -192,43 +187,49 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { consumed += length + 4; } - } finally { - if (consumed) wsBuffer.current = wsBuffer.current.slice(consumed); + } catch (err) { + NitroLogger.error(err); } + if (consumed) wsBuffer.current = wsBuffer.current.slice(consumed); + return wrappers; } - const processWrappers = (...wrappers: IMessageDataWrapper[]) => { + const processWrapper = (wrapper: IMessageDataWrapper) => { try { - if (!wrappers || !wrappers.length) return; - - for (const wrapper of wrappers) { - try { - const ctor = incomingByHeader.current.get(wrapper.header); + const ctor = incomingByHeader.current.get(wrapper.header); - if (!ctor) continue; + if (!ctor) return; - const handlers = listeners.current.get(ctor); + const handlers = listeners.current.get(ctor); - if (!handlers?.length) continue; + if (!handlers?.length) return; - const parsed = new ctor().parse(wrapper); + const parsed = new ctor().parse(wrapper); - for (const handle of handlers) handle(parsed); - } catch (err) { - NitroLogger.error(err); - } - } + for (const handle of handlers) handle(parsed); } catch (err) { NitroLogger.error(err); } } + const dispatchWrappers = (wrappers: IMessageDataWrapper[]) => { + for (let index = 0; index < wrappers.length; index++) { + if (phase.current === 'awaitingHandlers') { + pendingServerMessages.current.push(...wrappers.slice(index)); + + return; + } + + processWrapper(wrappers[index]); + } + } + const send = (...packets: IOutgoingPacket[]) => { if (!packets?.length) return; - if (isAuthenticated && !isReady) { + if (phase.current === 'awaitingHandlers') { pendingClientMessages.current.push(...packets); return; @@ -267,6 +268,12 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { } } + const setPhase = (next: ConnectionPhase) => { + phase.current = next; + + setRenderedPhase(next); + } + const subscribe = ( event: IncomingPacketConstructor, handler: (data: T) => void @@ -297,7 +304,7 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { }; const setReady = () => { - if (isReady) return; + if (phase.current !== 'awaitingHandlers') return; const pendingClient = pendingClientMessages.current; const pendingServer = pendingServerMessages.current; @@ -305,15 +312,17 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { pendingServerMessages.current = []; pendingClientMessages.current = []; - setIsReady(true); + setPhase('ready'); - processWrappers(...pendingServer); + dispatchWrappers(pendingServer); sendRaw(...pendingClient); } useEffect(() => { registerManyIncoming(GetIncomingPackets()); registerManyOutgoing(GetOutgoingPackets()); + + return subscribe(AuthenticationOKMessage, () => setPhase('awaitingHandlers')); }, []); useEffect(() => () => { @@ -324,13 +333,8 @@ export const WebSocketContextProvider = ({ children }: ProviderProps) => { socket?.close(1000, 'Client shutting down'); }, []); - useEffect(() => { - if (isAuthenticated) return; - - return subscribe(AuthenticationOKMessage, data => { - setIsAuthenticated(true); - }); - }, [isAuthenticated, subscribe]); + const isAuthenticated = renderedPhase === 'awaitingHandlers' || renderedPhase === 'ready'; + const isDisconnected = renderedPhase === 'closed'; return (