Hi all,
I’m looking to add direct messaging to my application using Remix.run with a custom Express server. Currently, I have two Fly.io apps running: the application itself and a PostgreSQL database. Both apps run on multiple instances.
I’m having trouble setting up Socket.io properly across these multiple instances. I’ve read the Fly.io blog article on websockets (WebSockets and Fly · The Fly Blog) and found this repository (GitHub - bjarkebech/express-socketio-fly-replay: Automatically replay Socket.io connections to the correct Fly machine using Node.js and express.), but I still can’t get it to work with my application.
Here’s my code based on the bjarkebech/express-socketio-fly-replay repository. I’m considering always connecting the socket to the same instance (machine) by setting a static machineID (target_instance).
Server:
const app = express()
const httpServer = createServer(app)
const FLY_INSTANCE_ID = process.env.FLY_ALLOC_ID
? process.env.FLY_ALLOC_ID.split('-')[0]
: null
const TARGET_INSTANCE = process.env.FLY_CHAT_MACHINE_ID
httpServer.on('upgrade', function (req, socket) {
console.log('Fly machine ID:', FLY_INSTANCE_ID)
console.log('Target machine ID:', TARGET_INSTANCE)
// Opt out on localhost/non-Fly environments.
if (!FLY_INSTANCE_ID) return
// Opt out on other HTTP upgrades.
if (req.headers['upgrade'] !== 'websocket') return
if (FLY_INSTANCE_ID === TARGET_INSTANCE) return
console.log('Mismatch detected, replaying')
// Create a raw HTTP response with the fly-replay header.
// HTTP code 101 must be used to make the response replay correctly.
const headers = [
'HTTP/1.1 101 Switching Protocols',
`fly-replay: instance=${TARGET_INSTANCE}`,
]
// Send new headers and close the socket.
socket.end(headers.concat('\r\n').join('\r\n'))
})
const io = new Server(httpServer)
io.on('connection', socket => {
socket.on('joinRoom', roomId => {
socket.join(roomId)
console.log(`User joined room: ${roomId}`)
})
socket.on('sendMessage', ({ roomId, message }) => {
io.to(roomId).emit('receiveMessage', message)
})
socket.on('disconnect', () => {
console.log('user disconnected')
})
})
Component:
// ...
// all imports
export async function loader({ request, params }: LoaderFunctionArgs) {
const userId = await requireUserId(request)
const roomId = params.roomId
invariantResponse(roomId, 'Not found', { status: 404 })
const room = await getRoom({ roomId, userId })
invariantResponse(room, 'Unauthorized', { status: 403 })
const messages = await getMessages(roomId)
const messagesByDay = organizeMessagesByDay(messages)
return json({
room,
userId,
messages: messagesByDay,
})
}
export async function action({
request,
context,
}: ActionFunctionArgs & { context: { io: Server } }) {
const userId = await requireUserId(request)
const formData = await request.formData()
const body = formData.get('body')
const roomId = formData.get('roomId')
const room = getRoom({ roomId, userId })
invariantResponse(room, 'Unauthorized', { status: 403 })
const message = await createMessage({ body, userId, roomId })
context.io.to(roomId).emit('receiveMessage', message)
return redirect(`/messages/${roomId}`)
}
export default function MessageUserPage() {
const { messages, room, userId } = useLoaderData<typeof loader>()
const [liveMessages, setLiveMessages] = useState<MessagesByDay>(messages)
useEffect(() => {
const socket = io()
socket.emit('joinRoom', room.id)
socket.on('receiveMessage', message => {
setLiveMessages(prevMessages => {
const date = new Date(message.createdAt).toISOString().split('T')[0]
if (date) {
return {
...prevMessages,
[date]: prevMessages[date]
? [...prevMessages[date]!, message]
: [message],
}
}
return prevMessages
})
})
return () => {
socket.disconnect()
}
}, [room.id])
return (
<>
<Header
left={people.map(
person => person.profile?.displayName ?? `@${person.username}`,
)}
/>
<ul className="flex-1 p-4 md:p-8">
{Object.entries(liveMessages).map(([date, messages]) => {
return (
<div key={date}>
<Badge className="mx-auto block w-fit" variant="secondary">
{date}
</Badge>
<ul className="my-8">
{messages.map(message => {
const isEmojiOnlyMessage = isEmojiOnly(message.body)
return (
<li key={message.id} className="peer">
<Message
{...message}
className={
isEmojiOnlyMessage
? 'bg-transparent p-0 text-[64px]'
: ''
}
>
{parse({ text: message.body })}
</Message>
</li>
)
})}
</ul>
</div>
)
})}
<li ref={messageEndRef} />
</ul>
<Footer>
<Form
action="."
method="POST"
ref={formRef}
preventScrollReset
className="flex w-full items-end gap-2"
>
<input type="hidden" name="roomId" value={room.id} />
<AutoResizeTextarea autoFocus />
<Button disabled={isPending} ref={buttonRef} type="submit">
Send
</Button>
</Form>
</Footer>
</>
)
}
However, this setup doesn’t work when I use a fly-prefer-region mod header in an incognito browser window to chat.
Does anyone know the proper way to connect two users on different machines via a Socket.io connection and make it possible for them to chat with each other? Any guidance on what I might be doing wrong would be greatly appreciated.
Thanks in advance!