diff --git a/src/hooks/events/index.ts b/src/hooks/events/index.ts index 6b5c11e..dab8992 100644 --- a/src/hooks/events/index.ts +++ b/src/hooks/events/index.ts @@ -1,6 +1,9 @@ export * from './useEventDispatcher'; +export * from './useExternalSnapshot'; export * from './useMessageEvent'; +export * from './useMessageEventReducer'; export * from './useMessageEventState'; export * from './useNitroEvent'; +export * from './useNitroEventReducer'; export * from './useNitroEventState'; export * from './useUiEvent'; diff --git a/src/hooks/events/useExternalSnapshot.ts b/src/hooks/events/useExternalSnapshot.ts new file mode 100644 index 0000000..8beb340 --- /dev/null +++ b/src/hooks/events/useExternalSnapshot.ts @@ -0,0 +1,25 @@ +import { useSyncExternalStore } from 'react'; + +/** + * useSyncExternalStore wrapper for the Nitro renderer's subscribe + snapshot + * getter contract. + * + * Pair with EventDispatcher.subscribe() (Nitro_Render_V3 v2.1.0+) and a + * referentially-stable snapshot getter such as + * SessionDataManager.getUserDataSnapshot() or + * RoomSessionManager.getActiveRoomSessionSnapshot(). + * + * const userData = useExternalSnapshot( + * cb => GetEventDispatcher().subscribe(NitroEventType.SESSION_DATA_UPDATED, cb), + * () => GetSessionDataManager().getUserDataSnapshot() + * ); + * + * Snapshot reference invariance is guaranteed by the renderer: the same + * object is returned across reads until the corresponding *_UPDATED event + * dispatches. + */ +export const useExternalSnapshot = ( + subscribe: (onChange: () => void) => () => void, + getSnapshot: () => T, + getServerSnapshot?: () => T +): T => useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot ?? getSnapshot); diff --git a/src/hooks/events/useMessageEventReducer.ts b/src/hooks/events/useMessageEventReducer.ts new file mode 100644 index 0000000..931d95c --- /dev/null +++ b/src/hooks/events/useMessageEventReducer.ts @@ -0,0 +1,65 @@ +import { GetCommunication, IMessageEvent, MessageEvent } from '@nitrots/nitro-renderer'; +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; + +/** + * Reducer companion of useMessageEventState for the server message + * channel. Accepts either a single event type or an array of event types + * that all collapse into the same state slice — typical for streams where + * different composers update the same shape (e.g. furniture list + + * add/update + remove all maintain one GroupItem[]). + * + * const groupItems = useMessageEventReducer( + * [ParserA, ParserB, ParserC], + * (state, event) => { + * if (event instanceof ParserA) return applyA(state, event); + * if (event instanceof ParserB) return applyB(state, event); + * return state; + * }, + * [] + * ); + * + * Closure stability via reducerRef refreshed in commit phase. + */ +export const useMessageEventReducer = ( + eventType: typeof MessageEvent | (typeof MessageEvent)[], + reducer: (state: S, event: T) => S, + initial: S | (() => S) +): S => +{ + const [ value, setValue ] = useState(initial); + const reducerRef = useRef(reducer); + + useLayoutEffect(() => + { + reducerRef.current = reducer; + }); + + const handler = useCallback((event: T) => + { + setValue(prev => reducerRef.current(prev, event)); + }, []); + + const types = useMemo(() => Array.isArray(eventType) ? eventType : [ eventType ], [ eventType ]); + + useEffect(() => + { + const communication = GetCommunication(); + + const registered = types.map(t => + { + //@ts-ignore + const event = new t(handler); + + communication.registerMessageEvent(event); + + return event; + }); + + return () => + { + for(const event of registered) communication.removeMessageEvent(event); + }; + }, [ types, handler ]); + + return value; +}; diff --git a/src/hooks/events/useNitroEventReducer.ts b/src/hooks/events/useNitroEventReducer.ts new file mode 100644 index 0000000..0756176 --- /dev/null +++ b/src/hooks/events/useNitroEventReducer.ts @@ -0,0 +1,52 @@ +import { NitroEvent } from '@nitrots/nitro-renderer'; +import { useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { useNitroEvent } from './useNitroEvent'; + +/** + * Reducer companion of useNitroEventState for the cases where multiple + * event types collapse into a single state slice. Replaces the pattern: + * + * const [state, setState] = useState(initial); + * useNitroEvent(EVENT_A, e => setState(prev => reduceA(prev, e))); + * useNitroEvent(EVENT_B, e => setState(prev => reduceB(prev, e))); + * + * with: + * + * const state = useNitroEventReducer( + * [EVENT_A, EVENT_B], + * (state, event) => { + * if (event instanceof EventA) return reduceA(state, event); + * if (event instanceof EventB) return reduceB(state, event); + * return state; + * }, + * initial + * ); + * + * Closure stability: the reducer ref is refreshed in commit phase, so a + * new reducer identity per render does not force the listener to + * re-subscribe. + */ +export const useNitroEventReducer = ( + types: string | string[], + reducer: (state: S, event: T) => S, + initial: S | (() => S), + enabled: boolean = true +): S => +{ + const [ value, setValue ] = useState(initial); + const reducerRef = useRef(reducer); + + useLayoutEffect(() => + { + reducerRef.current = reducer; + }); + + const handler = useCallback((event: T) => + { + setValue(prev => reducerRef.current(prev, event)); + }, []); + + useNitroEvent(types, handler, enabled); + + return value; +};