Get websockets working

This commit is contained in:
Kenneth Allen 2025-10-10 14:14:26 +11:00
parent bd14edadf7
commit 0299b5bb43
5 changed files with 1367 additions and 14 deletions

23
app/api/ws/route.ts Normal file
View File

@ -0,0 +1,23 @@
import type { NextRequest } from "next/server"
import type { WebSocket, WebSocketServer } from "ws"
export function UPGRADE(client: WebSocket, server: WebSocketServer, _req: NextRequest) {
client.send(JSON.stringify({ type: "server:hello", message: "Connected via next-ws" }))
client.on("message", (data) => {
let payload: any
try {
payload = JSON.parse(data.toString())
} catch {
return
}
if (payload?.type === "client:ping") {
const message = typeof payload.message === "string" ? payload.message : ""
const response = { type: "server:pong", message: `pong: ${message}`.trim() }
server.clients.forEach((peer) => {
if (peer.readyState === client.OPEN) peer.send(JSON.stringify(response))
})
}
})
}

93
app/components/WsDemo.tsx Normal file
View File

@ -0,0 +1,93 @@
"use client"
import { useEffect, useRef, useState } from "react"
export default function WsDemo() {
const [connected, setConnected] = useState(false)
const [messages, setMessages] = useState<string[]>([])
const [input, setInput] = useState("")
const wsRef = useRef<WebSocket | null>(null)
useEffect(() => {
const protocol = window.location.protocol === "https:" ? "wss" : "ws"
const url = `${protocol}://${window.location.host}/api/ws`
const existing = (window as any).__ws as WebSocket | undefined
const ws = existing && (existing.readyState === WebSocket.OPEN || existing.readyState === WebSocket.CONNECTING)
? existing
: new WebSocket(url)
;(window as any).__ws = ws
wsRef.current = ws
const onOpen = (event: Event) => {
console.log("onOpen", event)
setConnected(true)
}
const onClose = (event: Event) => {
console.log("onClose", event)
setConnected(false)
}
const onError = (event: Event) => {
console.log("onError", event)
setConnected(false)
}
const onMessage = (event: MessageEvent) => {
console.log("onMessage", event)
try {
const payload = JSON.parse(event.data)
if (payload?.type === "server:hello" || payload?.type === "server:pong") {
setMessages((prev) => [...prev, payload.message])
}
} catch {}
}
ws.addEventListener("open", onOpen)
ws.addEventListener("close", onClose)
ws.addEventListener("error", onError)
ws.addEventListener("message", onMessage)
const beforeUnload = () => { try { ws.close() } catch {} }
window.addEventListener("beforeunload", beforeUnload)
return () => {
console.log("unmounting")
ws.removeEventListener("open", onOpen)
ws.removeEventListener("close", onClose)
ws.removeEventListener("error", onError)
ws.removeEventListener("message", onMessage)
window.removeEventListener("beforeunload", beforeUnload)
}
}, [])
const sendPing = () => {
const msg = input || "ping"
wsRef.current?.send(JSON.stringify({ type: "client:ping", message: msg }))
setInput("")
}
return (
<div className="w-full max-w-xl rounded-md border border-neutral-300 dark:border-neutral-800 p-4 space-y-3">
<div className="flex items-center gap-2">
<span className={`inline-block w-2.5 h-2.5 rounded-full ${connected ? "bg-emerald-500" : "bg-rose-500"}`} />
<span className="font-semibold">WebSocket</span>
<span className="opacity-70">{connected ? "connected" : "disconnected"}</span>
</div>
<div className="flex gap-2">
<input
className="flex-1 rounded-md border border-neutral-300 dark:border-neutral-800 bg-transparent px-3 py-2"
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message"
/>
<button className="rounded-md border px-3 py-2" onClick={sendPing}>Send ping</button>
</div>
<ul className="space-y-1 text-sm">
{messages.map((m, i) => (
<li key={i}>{m}</li>
))}
</ul>
</div>
)
}

View File

@ -1,4 +1,5 @@
import Image from "next/image"; import Image from "next/image";
import WsDemo from "./components/WsDemo";
export default function Home() { export default function Home() {
return ( return (
@ -12,6 +13,7 @@ export default function Home() {
height={38} height={38}
priority priority
/> />
<WsDemo />
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left"> <ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
<li className="mb-2 tracking-[-.01em]"> <li className="mb-2 tracking-[-.01em]">
Get started by editing{" "} Get started by editing{" "}

1249
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,20 +7,24 @@
"build": "next build --turbopack", "build": "next build --turbopack",
"start": "next start", "start": "next start",
"lint": "biome check", "lint": "biome check",
"format": "biome format --write" "format": "biome format --write",
"prepare": "next-ws patch"
}, },
"dependencies": { "dependencies": {
"next": "^15.5.4",
"next-ws": "^2.1.3",
"react": "19.1.0", "react": "19.1.0",
"react-dom": "19.1.0", "react-dom": "19.1.0",
"next": "15.5.4" "ws": "^8.18.3"
}, },
"devDependencies": { "devDependencies": {
"typescript": "^5", "@biomejs/biome": "2.2.0",
"@tailwindcss/postcss": "^4",
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@tailwindcss/postcss": "^4", "@types/ws": "^8.18.1",
"tailwindcss": "^4", "tailwindcss": "^4",
"@biomejs/biome": "2.2.0" "typescript": "^5"
} }
} }