@noya-app/noya-multiplayer-react 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/hooks.ts","../src/globals.ts","../src/inspector/useStateInspector.tsx","../src/inspector/StateInspector.tsx","../src/inspector/useLocalStorageState.tsx"],"sourcesContent":["export * from \"@noya-app/state-manager\";\nexport * from \"./hooks\";\nexport * from \"./inspector/StateInspector\";\n","import {\n ActionHandler,\n ConnectionEvent,\n Get,\n MultiplayerPatchMetadata,\n MultiplayerStateManager,\n StateManager,\n StateManagerOptions,\n SyncAdapter,\n stubSync,\n} from \"@noya-app/state-manager\";\nimport {\n SetStateAction,\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n useSyncExternalStore,\n} from \"react\";\nimport { trackEvents, useObservable } from \"./globals\";\nimport { useStateInspector } from \"./inspector/useStateInspector\";\n\nexport function useSyncStateManager<S extends object, M>(\n stateManager: StateManager<S, M>\n): S;\nexport function useSyncStateManager<S extends object, M, P extends string>(\n stateManager: StateManager<S, M>,\n path: P\n): Get<S, P, unknown>;\nexport function useSyncStateManager<S extends object, M>(\n stateManager: StateManager<S, M>,\n path?: string\n) {\n const getSnapshot = useCallback(\n () =>\n typeof path === \"string\"\n ? stateManager.getState(path)\n : stateManager.getState(),\n [stateManager, path]\n );\n\n return useSyncExternalStore(\n stateManager.addListener,\n getSnapshot,\n getSnapshot\n );\n}\n\nexport function useManagedState<S extends object, M = void>(\n createInitialState: () => S,\n options?: StateManagerOptions<S, M>\n) {\n const [stateManager] = useState(\n () => new StateManager<S, M>(createInitialState(), options)\n );\n\n const state = useSyncStateManager(stateManager);\n\n const setState = useCallback(\n (\n ...args: M extends {}\n ? [metadata: M, action: SetStateAction<S>]\n : [action: SetStateAction<S>]\n ) => {\n const [metadata, action] =\n args.length === 1 ? [undefined, args[0]] : args;\n\n if (metadata === undefined) {\n const performActionNoMetadata = stateManager.performAction as (\n handler: ActionHandler<S>\n ) => void;\n\n if (typeof action === \"function\") {\n performActionNoMetadata({\n run: action,\n undo: () => state,\n });\n } else {\n performActionNoMetadata({\n run: () => action,\n undo: () => state,\n });\n }\n } else {\n const performActionWithMetadata = stateManager.performAction as (\n metadata: M,\n handler: ActionHandler<S>\n ) => void;\n\n if (typeof action === \"function\") {\n performActionWithMetadata(metadata, {\n run: action,\n undo: () => state,\n });\n } else {\n performActionWithMetadata(metadata, {\n run: () => action,\n undo: () => state,\n });\n }\n }\n },\n [state, stateManager]\n );\n\n return [state, setState, stateManager] as const;\n}\n\nexport function useManagedHistory<S extends object, M = void>(\n stateManager: StateManager<S, M>\n) {\n return useSyncExternalStore(\n stateManager.historyEmittor.addListener,\n stateManager.getHistorySnapshot,\n stateManager.getHistorySnapshot\n );\n}\n\nexport function useSyncMultiplayerStateManager<S extends object>(\n multiplayerStateManager: MultiplayerStateManager<S>\n) {\n return useSyncExternalStore(\n multiplayerStateManager.addListener,\n multiplayerStateManager.getOptimisticState,\n multiplayerStateManager.getOptimisticState\n );\n}\n\nconst defaultStubSync = stubSync();\n\nexport function useMultiplayerState<S extends object>(\n initialState: S | (() => S),\n options: StateManagerOptions<S, MultiplayerPatchMetadata> & {\n sync?: (self: MultiplayerStateManager<S>) => void;\n trackConnectionEvents?: boolean;\n inspector?: boolean | { colorScheme?: \"dark\" | \"light\" };\n }\n) {\n const { sync = defaultStubSync as unknown as SyncAdapter<S>, ...rest } =\n options;\n\n const [multiplayerStateManager] = useState(\n () => new MultiplayerStateManager<S>(initialState, rest)\n );\n\n const syncRef = useRef(sync);\n\n const [connectionEvents, setConnectionEvents] = useState<\n ConnectionEvent<S>[] | undefined\n >();\n\n const extras = useMemo(() => {\n return {\n connectionEvents,\n };\n }, [connectionEvents]);\n\n const globalTrackEvents = useObservable(trackEvents);\n const trackConnectionEvents =\n options.trackConnectionEvents ?? globalTrackEvents;\n\n const trackEventsCallbackRef = useRef<\n ((e: ConnectionEvent<S>) => void) | undefined\n >();\n\n useEffect(() => {\n trackEventsCallbackRef.current = trackConnectionEvents\n ? (e) =>\n setConnectionEvents((events) => {\n const updated = events ? [...events, e] : [e];\n return updated.length > 50 ? updated.slice(-50) : updated;\n })\n : undefined;\n }, [trackConnectionEvents]);\n\n useEffect(() => {\n if (syncRef.current) {\n return syncRef.current(multiplayerStateManager, (e) =>\n trackEventsCallbackRef.current?.(e)\n );\n }\n }, [multiplayerStateManager]);\n\n const state = useSyncMultiplayerStateManager(multiplayerStateManager);\n\n useStateInspector({\n stateManager: multiplayerStateManager.sm,\n state,\n connectionEvents,\n disabled: !options.inspector,\n colorScheme:\n typeof options.inspector === \"object\"\n ? options.inspector.colorScheme\n : undefined,\n });\n\n return [\n state,\n multiplayerStateManager.setOptimisticState,\n multiplayerStateManager,\n extras,\n ] as const;\n}\n","import { Emitter } from \"@noya-app/emitter\";\nimport { useEffect, useState } from \"react\";\n\nclass Observable<T> {\n private value: T;\n private emitter = new Emitter<[T]>();\n\n constructor(value: T) {\n this.value = value;\n }\n\n get() {\n return this.value;\n }\n\n set(value: T) {\n if (this.value === value) return;\n\n this.value = value;\n\n this.emitter.emit(value);\n }\n\n listen(callback: (value: T) => void) {\n callback(this.value);\n\n return this.emitter.addListener(callback);\n }\n}\n\nexport function useObservable<T>(observable: Observable<T>) {\n const [value, setValue] = useState(observable.get());\n\n useEffect(() => {\n return observable.listen(setValue);\n }, [observable]);\n\n return value;\n}\n\nexport const trackEvents = new Observable(false);\n","import { ConnectionEvent, StateManager } from \"@noya-app/state-manager\";\nimport React, { ComponentProps, useLayoutEffect } from \"react\";\nimport { createRoot } from \"react-dom/client\";\n\nimport { Root } from \"react-dom/client\";\nimport { StateInspector } from \"./StateInspector\";\n\nfunction createPortalElement() {\n const el = document.createElement(\"div\");\n el.id = \"noya-multiplayer-react-inspector-portal\";\n return el;\n}\n\nexport function useStateInspector<S extends object, M>({\n state,\n connectionEvents,\n disabled = false,\n colorScheme,\n stateManager,\n}: {\n state: S;\n connectionEvents?: ConnectionEvent<S>[];\n disabled: boolean;\n colorScheme: ComponentProps<typeof StateInspector>[\"colorScheme\"];\n stateManager: StateManager<S, M>;\n}) {\n const [root, setRoot] = React.useState<Root | null>(null);\n\n useLayoutEffect(() => {\n if (disabled) return;\n\n const portalElement = createPortalElement();\n\n const root = createRoot(portalElement);\n\n document.body.appendChild(portalElement);\n\n setRoot(root);\n\n return () => {\n document.body.removeChild(portalElement);\n };\n }, [disabled]);\n\n useLayoutEffect(() => {\n if (!root) return;\n\n root.render(\n <StateInspector\n state={state}\n connectionEvents={connectionEvents}\n colorScheme={colorScheme}\n stateManager={stateManager as StateManager<any, any>}\n />\n );\n }, [colorScheme, connectionEvents, root, state, stateManager]);\n}\n","import {\n ConnectionEvent,\n ExtendedPathKey,\n StateManager,\n} from \"@noya-app/state-manager\";\nimport React, {\n CSSProperties,\n ComponentPropsWithoutRef,\n FC,\n memo,\n useEffect,\n useLayoutEffect,\n} from \"react\";\nimport {\n ObjectInspector,\n ObjectLabel,\n ObjectName,\n ObjectPreview,\n chromeDark,\n chromeLight,\n} from \"react-inspector\";\nimport { trackEvents } from \"../globals\";\nimport { useManagedHistory } from \"../hooks\";\nimport { useLocalStorageState } from \"./useLocalStorageState\";\n\nconst lightTheme = {\n ...chromeLight,\n BASE_BACKGROUND_COLOR: \"transparent\",\n OBJECT_NAME_COLOR: \"rgba(0,0,0,0.7)\",\n} as any;\n\nconst darkTheme = {\n ...chromeDark,\n BASE_BACKGROUND_COLOR: \"transparent\",\n OBJECT_NAME_COLOR: \"rgba(255,255,255,0.7)\",\n} as any;\n\nfunction ToggleButton({\n showInspector,\n setShowInspector,\n theme,\n}: {\n showInspector: boolean;\n setShowInspector: (value: boolean) => void;\n theme: typeof chromeLight;\n}) {\n return (\n <span\n role=\"button\"\n style={{\n flex: \"0\",\n appearance: \"none\",\n color: theme.BASE_COLOR,\n background: \"rgba(0,0,0,0.1)\",\n border: \"none\",\n fontSize: \"12px\",\n whiteSpace: \"nowrap\",\n display: \"inline-flex\",\n alignItems: \"center\",\n justifyContent: \"center\",\n padding: \"2px 0\",\n }}\n onClick={() => setShowInspector(!showInspector)}\n >\n <span style={{ width: \"12px\", height: \"12px\" }}>\n {showInspector ? (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n strokeWidth={1.5}\n stroke=\"currentColor\"\n className=\"size-6\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"m5.25 4.5 7.5 7.5-7.5 7.5m6-15 7.5 7.5-7.5 7.5\"\n />\n </svg>\n ) : (\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n strokeWidth={1.5}\n stroke=\"currentColor\"\n className=\"size-6\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n d=\"m18.75 4.5-7.5 7.5 7.5 7.5m-6-15L5.25 12l7.5 7.5\"\n />\n </svg>\n )}\n </span>\n </span>\n );\n}\n\nfunction DisclosureSection({\n open,\n setOpen,\n title,\n right,\n children,\n colorScheme,\n isFirst,\n}: {\n open: boolean;\n setOpen?: (value: boolean) => void;\n title: string;\n right?: React.ReactNode;\n children: React.ReactNode;\n colorScheme: \"light\" | \"dark\";\n isFirst?: boolean;\n}) {\n const theme = colorScheme === \"light\" ? lightTheme : darkTheme;\n\n const borderColor =\n colorScheme === \"light\" ? \"rgba(0,0,0,0.1)\" : \"rgba(255,255,255,0.1)\";\n\n return (\n <div\n style={{\n flex: open ? \"1 1 0\" : \"0\",\n display: \"flex\",\n flexDirection: \"column\",\n }}\n >\n <div\n onClick={() => setOpen?.(!open)}\n style={{\n cursor: \"default\",\n fontSize: \"12px\",\n padding: \"4px 10px\",\n display: \"flex\",\n ...(!isFirst && { borderTop: `1px solid ${borderColor}` }),\n ...(open && { borderBottom: `1px solid ${borderColor}` }),\n }}\n >\n {setOpen && (\n <Arrow\n expanded={open}\n style={{\n fontSize: \"11px\",\n // fontSize: theme.ARROW_FONT_SIZE,\n marginRight: theme.ARROW_MARGIN_RIGHT,\n color: theme.ARROW_COLOR,\n }}\n />\n )}\n <span style={{ flex: \"1 1 0\" }}>{title}</span>\n {right}\n </div>\n {open && children}\n </div>\n );\n}\n\nexport const StateInspector = memo(function StateInspector<\n S extends object,\n M,\n>({\n state,\n connectionEvents,\n unstyled,\n colorScheme = \"light\",\n stateManager,\n ...props\n}: {\n state: S;\n connectionEvents?: ConnectionEvent<S>[];\n unstyled?: boolean;\n colorScheme?: \"light\" | \"dark\";\n stateManager: StateManager<S, M>;\n} & ComponentPropsWithoutRef<\"div\">) {\n const [didMount, setDidMount] = React.useState(false);\n\n useLayoutEffect(() => {\n setDidMount(true);\n }, []);\n\n const eventsContainerRef = React.useRef<HTMLDivElement>(null);\n const historyContainerRef = React.useRef<HTMLDivElement>(null);\n\n const [showInspector, setShowInspector] = useLocalStorageState(\n \"noya-multiplayer-react-show-inspector\",\n true\n );\n const [showEvents, setShowEvents] = useLocalStorageState(\n \"noya-multiplayer-react-show-events\",\n false\n );\n const [showHistory, setShowHistory] = useLocalStorageState(\n \"noya-multiplayer-react-show-history\",\n false\n );\n\n useEffect(() => {\n trackEvents.set(showEvents);\n }, [showEvents]);\n\n useEffect(() => {\n if (eventsContainerRef.current) {\n eventsContainerRef.current.scrollTop =\n eventsContainerRef.current.scrollHeight;\n }\n }, [connectionEvents]);\n\n const historySnapshot = useManagedHistory(stateManager);\n\n useEffect(() => {\n if (historyContainerRef.current) {\n historyContainerRef.current.scrollTop =\n historyContainerRef.current.scrollHeight;\n }\n }, [historySnapshot]);\n\n // If history index changes, scroll to the new history entry into view\n useEffect(() => {\n if (!historyContainerRef.current) return;\n\n const historyEntry = historyContainerRef.current.querySelector(\n `#history-${historySnapshot.historyIndex - 1}`\n );\n\n if (historyEntry) {\n historyEntry.scrollIntoView({\n block: \"nearest\",\n inline: \"nearest\",\n });\n }\n }, [historySnapshot.historyIndex]);\n\n const theme = colorScheme === \"light\" ? lightTheme : darkTheme;\n const borderColor =\n colorScheme === \"light\" ? \"rgba(0,0,0,0.1)\" : \"rgba(255,255,255,0.1)\";\n\n const baseStyle: CSSProperties = {\n position: \"fixed\",\n top: 12,\n right: 12,\n bottom: 12,\n width: 400,\n background:\n colorScheme === \"light\" ? \"rgba(255,255,255,0.95)\" : \"rgba(0,0,0,0.95)\",\n backdropFilter: \"blur(48px)\",\n border: `1px solid ${borderColor}`,\n // outlineOffset: -1,\n borderRadius: 4,\n display: \"flex\",\n flexDirection: \"column\",\n // gap: 12,\n color: theme.BASE_COLOR,\n overflow: \"hidden\",\n zIndex: 9999,\n };\n\n if (!didMount) return null;\n\n if (!showInspector) {\n return (\n <div\n {...props}\n style={{\n ...baseStyle,\n padding: \"4px 10px\",\n borderRadius: 8,\n bottom: undefined,\n width: undefined,\n }}\n >\n <ToggleButton\n showInspector={showInspector}\n setShowInspector={setShowInspector}\n theme={theme}\n />\n </div>\n );\n }\n\n return (\n <div\n {...props}\n style={{\n ...(!unstyled && baseStyle),\n ...props.style,\n }}\n >\n {/* <div style={{ flex: \"0\", fontSize: \"12px\" }}>User ID: {userId}</div> */}\n <DisclosureSection\n open\n title=\"Data\"\n colorScheme={colorScheme}\n isFirst\n right={\n <ToggleButton\n showInspector={showInspector}\n setShowInspector={setShowInspector}\n theme={theme}\n />\n }\n >\n <div\n style={{\n flex: \"1 1 0\",\n overflowY: \"auto\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"1px\",\n padding: \"1px 12px\",\n }}\n >\n <ObjectInspector data={state} theme={theme} />\n </div>\n </DisclosureSection>\n <DisclosureSection\n open={showHistory}\n setOpen={setShowHistory}\n title=\"History\"\n colorScheme={colorScheme}\n >\n <div\n ref={historyContainerRef}\n style={{\n flex: \"1 1 0\",\n overflowY: \"auto\",\n overflowX: \"hidden\",\n display: \"flex\",\n flexDirection: \"column\",\n }}\n >\n {historySnapshot.history.length < 50 && (\n <div\n id={`history-${0}`}\n style={{\n borderBottom: `1px solid ${borderColor}`,\n fontSize: \"12px\",\n fontFamily: \"Menlo, monospace\",\n padding: \"2px 12px 1px\",\n display: \"flex\",\n alignItems: \"center\",\n background:\n historySnapshot.historyIndex === 0\n ? colorScheme === \"light\"\n ? \"rgba(0,0,0,0.2)\"\n : \"rgb(59 130 246 / 38%)\"\n : undefined,\n }}\n >\n <span\n style={{\n fontFamily: \"Menlo, monospace\",\n fontSize: \"11px\",\n borderRadius: 4,\n display: \"inline-block\",\n }}\n >\n Initial state\n </span>\n </div>\n )}\n {historySnapshot.history.slice(-50).map((entry, index) => (\n <div\n id={`history-${index}`}\n key={index}\n style={{\n padding: \"0px 12px 1px\",\n borderBottom: `1px solid ${borderColor}`,\n background:\n index === historySnapshot.historyIndex - 1\n ? colorScheme === \"light\"\n ? \"rgba(0,0,0,0.2)\"\n : \"rgb(59 130 246 / 15%)\"\n : undefined,\n }}\n >\n {entry.redoPatches?.map((patch, j) => (\n <pre\n key={j}\n style={{\n fontSize: \"11px\",\n display: \"flex\",\n flexWrap: \"wrap\",\n }}\n >\n {patch.op === \"add\" && (\n <>\n <span style={{ fontStyle: \"italic\" }}>\n {pathToString(patch.path)}\n </span>\n {\" = \"}\n <ObjectInspector data={patch.value} theme={theme} />\n </>\n )}\n {patch.op === \"replace\" && (\n <>\n <span style={{ fontStyle: \"italic\" }}>\n {pathToString(patch.path)}\n </span>\n {\" = \"}\n <ObjectInspector data={patch.value} theme={theme} />\n </>\n )}\n {patch.op === \"remove\" && (\n <>\n <span style={{ color: theme.OBJECT_VALUE_STRING_COLOR }}>\n delete{\" \"}\n </span>\n <span style={{ fontStyle: \"italic\" }}>\n {pathToString(patch.path)}\n </span>\n </>\n )}\n </pre>\n ))}\n </div>\n ))}\n </div>\n </DisclosureSection>\n <DisclosureSection\n open={showEvents}\n setOpen={setShowEvents}\n title=\"Events\"\n colorScheme={colorScheme}\n >\n <div\n ref={eventsContainerRef}\n style={{\n flex: \"1 1 0\",\n overflowY: \"auto\",\n display: \"flex\",\n flexDirection: \"column\",\n }}\n >\n {connectionEvents?.map((event, index) => (\n <div\n key={index}\n style={{\n borderBottom: `1px solid ${borderColor}`,\n }}\n >\n {event.type === \"stateChange\" ? (\n <div\n style={{\n padding: \"2px 12px\",\n background:\n colorScheme === \"light\"\n ? \"rgba(0,0,0,0.05)\"\n : \"rgba(255,255,255,0.05)\",\n display: \"flex\",\n alignItems: \"center\",\n }}\n >\n <span\n style={{\n fontFamily: \"Menlo, monospace\",\n fontSize: \"11px\",\n borderRadius: 4,\n display: \"inline-block\",\n }}\n >\n connection:{\" \"}\n <span style={{ fontStyle: \"italic\" }}>\n {event.state.toLowerCase()}\n </span>\n </span>\n </div>\n ) : (\n <div\n style={{\n padding: \"0px 12px 1px\",\n background:\n event.type === \"send\"\n ? \"rgba(0,255,0,0.1)\"\n : \"rgba(255,0,0,0.1)\",\n }}\n >\n <ObjectInspector\n data={event.message}\n theme={theme}\n nodeRenderer={({\n depth,\n name,\n data,\n isNonenumerable,\n expanded,\n }: any) => {\n const direction = event.type === \"send\" ? \"up\" : \"down\";\n\n return depth === 0 ? (\n <ObjectRootLabel direction={direction} data={data} />\n ) : (\n <ObjectLabel\n direction={direction}\n name={name}\n data={data}\n isNonenumerable={isNonenumerable}\n />\n );\n }}\n />\n </div>\n )}\n </div>\n ))}\n {!connectionEvents && (\n <div\n style={{\n padding: \"12px\",\n fontSize: \"12px\",\n display: \"flex\",\n flexDirection: \"column\",\n gap: \"4px\",\n }}\n >\n <span>No recorded events</span>\n </div>\n )}\n </div>\n </DisclosureSection>\n </div>\n );\n});\n\ntype MessageDirection = \"up\" | \"down\";\n\nconst ObjectRootLabel: FC<{\n name?: string;\n data: any;\n direction?: MessageDirection;\n}> = ({ name, data, direction }) => {\n if (typeof name === \"string\") {\n return (\n <span>\n <ObjectName name={name} />\n <span>: </span>\n <ObjectPreview data={data} />\n </span>\n );\n }\n if (direction === \"up\" || direction === \"down\") {\n const arrow = direction === \"up\" ? \"↑\" : \"↓\";\n\n return (\n <span>\n <span\n style={{\n background:\n direction === \"up\" ? \"rgba(0,255,0,0.2)\" : \"rgba(255,0,0,0.2)\",\n // color: \"white\",\n width: 12,\n height: 12,\n borderRadius: 2,\n display: \"inline-flex\",\n justifyContent: \"center\",\n alignItems: \"center\",\n marginRight: 4,\n lineHeight: \"12px\",\n }}\n >\n {arrow}\n </span>\n <ObjectPreview data={data} />\n </span>\n );\n } else {\n return <ObjectPreview data={data} />;\n }\n};\n\n// Rotates ▶️ or ▼ based on expanded state\nfunction Arrow({\n expanded,\n onClick,\n style,\n}: {\n expanded: boolean;\n onClick?: () => void;\n style?: CSSProperties;\n}) {\n return (\n <span\n role=\"button\"\n onClick={onClick}\n style={{\n display: \"inline-block\",\n textAlign: \"center\",\n transform: expanded ? \"rotate(0deg)\" : \"rotate(-90deg)\",\n ...style,\n }}\n >\n {\"▼\"}\n </span>\n );\n}\n\n// <div className=\"flex py-px flex-col-reverse\">\n// <button\n// type=\"button\"\n// className={cx(\n// \"px-4 py-2 cursor-pointer text-left\",\n// 0 === historySnapshot.historyIndex && \"bg-orange-500/25\"\n// )}\n// onClick={() => {\n// // stateManager.goToHistoryIndex(0);\n// }}\n// >\n// <div className=\"flex justify-between gap-2\">\n// <span>Initial State</span>\n// </div>\n// </button>\n// {historySnapshot.history.map((entry, index) => (\n// <button\n// key={index}\n// type=\"button\"\n// className={cx(\n// \"px-4 py-2 cursor-pointer text-left\",\n// index === historySnapshot.historyIndex - 1 &&\n// \"bg-orange-500/25\"\n// )}\n// onClick={() => {\n// // stateManager.goToHistoryIndex(index + 1);\n// }}\n// >\n// <div className=\"flex justify-between gap-2\">\n// <span>\n// {index + 1}. {entry.metadata.name}\n// </span>\n// <span>\n// {new Date(\n// entry.metadata.timestamp\n// ).toLocaleTimeString()}\n// </span>\n// </div>\n// <div className=\"font-mono text-xs\">\n// {entry.redoPatches?.map((patch, index) => (\n// <pre key={index} className=\"max-w-full\">\n// {patch.op === \"remove\"\n// ? `Delete ${pathToString(patch.path)}`\n// : `${pathToString(patch.path)} = ${JSON.stringify(\n// patch.value,\n// null,\n// 2\n// )}`}\n// </pre>\n// ))}\n// </div>\n// </button>\n// ))}\n// </div>\n\nfunction pathToString(extendedPath: ExtendedPathKey[]) {\n return extendedPath\n .map((key) =>\n typeof key === \"object\"\n ? `:${ellipsis(key.id.toString(), 8, \"middle\")}`\n : key\n )\n .map((key, index) =>\n index === 0 || (typeof key === \"string\" && key.startsWith(\":\"))\n ? key\n : `.${key}`\n )\n .join(\"\");\n}\n\ntype EllipsisPosition = \"start\" | \"middle\" | \"end\";\n\nfunction ellipsis(\n str: string,\n maxLength: number,\n position: EllipsisPosition = \"end\"\n) {\n if (str.length <= maxLength) return str;\n\n switch (position) {\n case \"start\":\n return `…${str.slice(str.length - maxLength)}`;\n case \"middle\":\n const halfLength = Math.floor(maxLength / 2);\n return `${str.slice(0, halfLength)}…${str.slice(str.length - halfLength)}`;\n case \"end\":\n return `${str.slice(0, maxLength)}…`;\n }\n}\n","import React from \"react\";\n\nconst localStorage = typeof window !== \"undefined\" ? window.localStorage : null;\n\nexport function useLocalStorageState<S>(\n key: string,\n defaultValue: S\n): [S, (value: S) => void] {\n const [state, setState] = React.useState<S>(() => {\n const value = localStorage?.getItem(key);\n let result = defaultValue;\n if (value) {\n try {\n result = JSON.parse(value);\n } catch (error) {\n console.error(\"Error parsing localStorage value\", error);\n }\n }\n return result;\n });\n\n const setLocalStorageState = (value: S) => {\n localStorage?.setItem(key, JSON.stringify(value));\n setState(value);\n };\n\n return [state, setLocalStorageState];\n}\n"],"mappings":"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","names":["useEffect","useState","React","useLayoutEffect","React","useEffect","React","StateInspector","useEffect","React","useLayoutEffect","root","useState","useEffect"]}
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@noya-app/noya-multiplayer-react",
3
+ "version": "0.1.9",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.mjs",
6
+ "types": "./dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsup src/index.ts --format cjs,esm --dts --sourcemap",
9
+ "dev": "npm run build -- --watch"
10
+ },
11
+ "dependencies": {
12
+ "@noya-app/state-manager": "0.1.9",
13
+ "@noya-app/emitter": "0.1.0",
14
+ "react-inspector": "6.0.2"
15
+ },
16
+ "peerDependencies": {
17
+ "react": "*",
18
+ "react-dom": "*"
19
+ }
20
+ }
package/src/globals.ts ADDED
@@ -0,0 +1,41 @@
1
+ import { Emitter } from "@noya-app/emitter";
2
+ import { useEffect, useState } from "react";
3
+
4
+ class Observable<T> {
5
+ private value: T;
6
+ private emitter = new Emitter<[T]>();
7
+
8
+ constructor(value: T) {
9
+ this.value = value;
10
+ }
11
+
12
+ get() {
13
+ return this.value;
14
+ }
15
+
16
+ set(value: T) {
17
+ if (this.value === value) return;
18
+
19
+ this.value = value;
20
+
21
+ this.emitter.emit(value);
22
+ }
23
+
24
+ listen(callback: (value: T) => void) {
25
+ callback(this.value);
26
+
27
+ return this.emitter.addListener(callback);
28
+ }
29
+ }
30
+
31
+ export function useObservable<T>(observable: Observable<T>) {
32
+ const [value, setValue] = useState(observable.get());
33
+
34
+ useEffect(() => {
35
+ return observable.listen(setValue);
36
+ }, [observable]);
37
+
38
+ return value;
39
+ }
40
+
41
+ export const trackEvents = new Observable(false);
package/src/hooks.ts ADDED
@@ -0,0 +1,204 @@
1
+ import {
2
+ ActionHandler,
3
+ ConnectionEvent,
4
+ Get,
5
+ MultiplayerPatchMetadata,
6
+ MultiplayerStateManager,
7
+ StateManager,
8
+ StateManagerOptions,
9
+ SyncAdapter,
10
+ stubSync,
11
+ } from "@noya-app/state-manager";
12
+ import {
13
+ SetStateAction,
14
+ useCallback,
15
+ useEffect,
16
+ useMemo,
17
+ useRef,
18
+ useState,
19
+ useSyncExternalStore,
20
+ } from "react";
21
+ import { trackEvents, useObservable } from "./globals";
22
+ import { useStateInspector } from "./inspector/useStateInspector";
23
+
24
+ export function useSyncStateManager<S extends object, M>(
25
+ stateManager: StateManager<S, M>
26
+ ): S;
27
+ export function useSyncStateManager<S extends object, M, P extends string>(
28
+ stateManager: StateManager<S, M>,
29
+ path: P
30
+ ): Get<S, P, unknown>;
31
+ export function useSyncStateManager<S extends object, M>(
32
+ stateManager: StateManager<S, M>,
33
+ path?: string
34
+ ) {
35
+ const getSnapshot = useCallback(
36
+ () =>
37
+ typeof path === "string"
38
+ ? stateManager.getState(path)
39
+ : stateManager.getState(),
40
+ [stateManager, path]
41
+ );
42
+
43
+ return useSyncExternalStore(
44
+ stateManager.addListener,
45
+ getSnapshot,
46
+ getSnapshot
47
+ );
48
+ }
49
+
50
+ export function useManagedState<S extends object, M = void>(
51
+ createInitialState: () => S,
52
+ options?: StateManagerOptions<S, M>
53
+ ) {
54
+ const [stateManager] = useState(
55
+ () => new StateManager<S, M>(createInitialState(), options)
56
+ );
57
+
58
+ const state = useSyncStateManager(stateManager);
59
+
60
+ const setState = useCallback(
61
+ (
62
+ ...args: M extends {}
63
+ ? [metadata: M, action: SetStateAction<S>]
64
+ : [action: SetStateAction<S>]
65
+ ) => {
66
+ const [metadata, action] =
67
+ args.length === 1 ? [undefined, args[0]] : args;
68
+
69
+ if (metadata === undefined) {
70
+ const performActionNoMetadata = stateManager.performAction as (
71
+ handler: ActionHandler<S>
72
+ ) => void;
73
+
74
+ if (typeof action === "function") {
75
+ performActionNoMetadata({
76
+ run: action,
77
+ undo: () => state,
78
+ });
79
+ } else {
80
+ performActionNoMetadata({
81
+ run: () => action,
82
+ undo: () => state,
83
+ });
84
+ }
85
+ } else {
86
+ const performActionWithMetadata = stateManager.performAction as (
87
+ metadata: M,
88
+ handler: ActionHandler<S>
89
+ ) => void;
90
+
91
+ if (typeof action === "function") {
92
+ performActionWithMetadata(metadata, {
93
+ run: action,
94
+ undo: () => state,
95
+ });
96
+ } else {
97
+ performActionWithMetadata(metadata, {
98
+ run: () => action,
99
+ undo: () => state,
100
+ });
101
+ }
102
+ }
103
+ },
104
+ [state, stateManager]
105
+ );
106
+
107
+ return [state, setState, stateManager] as const;
108
+ }
109
+
110
+ export function useManagedHistory<S extends object, M = void>(
111
+ stateManager: StateManager<S, M>
112
+ ) {
113
+ return useSyncExternalStore(
114
+ stateManager.historyEmittor.addListener,
115
+ stateManager.getHistorySnapshot,
116
+ stateManager.getHistorySnapshot
117
+ );
118
+ }
119
+
120
+ export function useSyncMultiplayerStateManager<S extends object>(
121
+ multiplayerStateManager: MultiplayerStateManager<S>
122
+ ) {
123
+ return useSyncExternalStore(
124
+ multiplayerStateManager.addListener,
125
+ multiplayerStateManager.getOptimisticState,
126
+ multiplayerStateManager.getOptimisticState
127
+ );
128
+ }
129
+
130
+ const defaultStubSync = stubSync();
131
+
132
+ export function useMultiplayerState<S extends object>(
133
+ initialState: S | (() => S),
134
+ options: StateManagerOptions<S, MultiplayerPatchMetadata> & {
135
+ sync?: (self: MultiplayerStateManager<S>) => void;
136
+ trackConnectionEvents?: boolean;
137
+ inspector?: boolean | { colorScheme?: "dark" | "light" };
138
+ }
139
+ ) {
140
+ const { sync = defaultStubSync as unknown as SyncAdapter<S>, ...rest } =
141
+ options;
142
+
143
+ const [multiplayerStateManager] = useState(
144
+ () => new MultiplayerStateManager<S>(initialState, rest)
145
+ );
146
+
147
+ const syncRef = useRef(sync);
148
+
149
+ const [connectionEvents, setConnectionEvents] = useState<
150
+ ConnectionEvent<S>[] | undefined
151
+ >();
152
+
153
+ const extras = useMemo(() => {
154
+ return {
155
+ connectionEvents,
156
+ };
157
+ }, [connectionEvents]);
158
+
159
+ const globalTrackEvents = useObservable(trackEvents);
160
+ const trackConnectionEvents =
161
+ options.trackConnectionEvents ?? globalTrackEvents;
162
+
163
+ const trackEventsCallbackRef = useRef<
164
+ ((e: ConnectionEvent<S>) => void) | undefined
165
+ >();
166
+
167
+ useEffect(() => {
168
+ trackEventsCallbackRef.current = trackConnectionEvents
169
+ ? (e) =>
170
+ setConnectionEvents((events) => {
171
+ const updated = events ? [...events, e] : [e];
172
+ return updated.length > 50 ? updated.slice(-50) : updated;
173
+ })
174
+ : undefined;
175
+ }, [trackConnectionEvents]);
176
+
177
+ useEffect(() => {
178
+ if (syncRef.current) {
179
+ return syncRef.current(multiplayerStateManager, (e) =>
180
+ trackEventsCallbackRef.current?.(e)
181
+ );
182
+ }
183
+ }, [multiplayerStateManager]);
184
+
185
+ const state = useSyncMultiplayerStateManager(multiplayerStateManager);
186
+
187
+ useStateInspector({
188
+ stateManager: multiplayerStateManager.sm,
189
+ state,
190
+ connectionEvents,
191
+ disabled: !options.inspector,
192
+ colorScheme:
193
+ typeof options.inspector === "object"
194
+ ? options.inspector.colorScheme
195
+ : undefined,
196
+ });
197
+
198
+ return [
199
+ state,
200
+ multiplayerStateManager.setOptimisticState,
201
+ multiplayerStateManager,
202
+ extras,
203
+ ] as const;
204
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from "@noya-app/state-manager";
2
+ export * from "./hooks";
3
+ export * from "./inspector/StateInspector";