start playing with websockets
This commit is contained in:
@ -1,8 +1,9 @@
|
||||
import { Link, useParams } from "react-router-dom"
|
||||
import { Link, useParams, useSearchParams } from "react-router-dom"
|
||||
import { Pico8Console } from "./pico8-client/Pico8Console";
|
||||
import { useEffect, useState } from "react";
|
||||
import { DbRelease } from "../server/dbal/dbal";
|
||||
import { css } from "@emotion/css";
|
||||
import { useWebsocket } from "./hooks/useWebsocket";
|
||||
|
||||
type Info = {
|
||||
release: DbRelease | null;
|
||||
@ -11,7 +12,15 @@ type Info = {
|
||||
|
||||
export const GamePage = () => {
|
||||
const {author, slug} = useParams();
|
||||
// const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const room = searchParams.get('room');
|
||||
const socket = useWebsocket({
|
||||
url: `/api/ws/room?room=${room}`,
|
||||
// url: "wss://echo.websocket.org",
|
||||
onMessage({message}) {
|
||||
console.log('message', message);
|
||||
}
|
||||
})
|
||||
// const version = searchParams.get('v');
|
||||
const [v, setVersion] = useState<string | null>(null);
|
||||
const [info, setInfo] = useState<Info | null>(null);
|
||||
@ -87,6 +96,11 @@ export const GamePage = () => {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button onClick={() => {
|
||||
room && socket.sendMessage({room, type: "hello", name: "world"});
|
||||
}}>Websocket</button>
|
||||
</div>
|
||||
{/* <div>
|
||||
<p>This is a paragraph about this game. It is a cool game. And a cool website to play it on. It automagically connects from GitHub.</p>
|
||||
</div> */}
|
||||
|
46
src/client/hooks/useWebsocket.ts
Normal file
46
src/client/hooks/useWebsocket.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
export const useWebsocket = (props: {url: string; onMessage: (stuff: {socket: WebSocket; message: unknown}) => void}) => {
|
||||
const {url, onMessage} = props;
|
||||
const onMessageRef = useRef(onMessage);
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
onMessageRef.current = onMessage;
|
||||
|
||||
useEffect(() => {
|
||||
const webSocket = new WebSocket(url);
|
||||
|
||||
webSocket.addEventListener("open", () => {
|
||||
console.log("WebSocket is open now.");
|
||||
});
|
||||
|
||||
webSocket.addEventListener("message", (event: any) => {
|
||||
onMessageRef.current({socket: webSocket, message: JSON.parse(event.data)});
|
||||
});
|
||||
|
||||
webSocket.addEventListener("error", (event) => {
|
||||
console.log("WebSocket error: ", event);
|
||||
});
|
||||
|
||||
webSocket.addEventListener("close", () => {
|
||||
console.log("WebSocket is closed now.");
|
||||
ws.current = null;
|
||||
});
|
||||
|
||||
ws.current = webSocket;
|
||||
|
||||
return () => {
|
||||
webSocket.close();
|
||||
};
|
||||
|
||||
}, [url]);
|
||||
|
||||
const sendMessage = useCallback((message: unknown) => {
|
||||
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(message));
|
||||
} else {
|
||||
console.error("WebSocket is not open. Message not sent.");
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {sendMessage};
|
||||
};
|
74
src/server/api/room.ts
Normal file
74
src/server/api/room.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import { FirRouteOptions, FirWebsocketHandler, FirWebsocketInput } from "../util/routewrap.js";
|
||||
import { WebSocket } from "@fastify/websocket";
|
||||
|
||||
const method = "GET";
|
||||
const url = "/api/ws/room";
|
||||
|
||||
const payloadT = Type.Any();
|
||||
|
||||
type Room = {
|
||||
name: string;
|
||||
sockets: WebSocket[];
|
||||
}
|
||||
|
||||
let rooms: Room[] = [];
|
||||
|
||||
const websocket = {
|
||||
onOpen({socket, req}) {
|
||||
const {room: roomName} = req.query as any;
|
||||
let room = rooms.find(r => r.name === roomName);
|
||||
if (!room) {
|
||||
console.log("creating room", roomName);
|
||||
room = {
|
||||
name: roomName,
|
||||
sockets: [],
|
||||
};
|
||||
rooms.push(room);
|
||||
}
|
||||
if (!room.sockets.includes(socket)) {
|
||||
console.log("adding socket to room", roomName);
|
||||
room.sockets.push(socket);
|
||||
}
|
||||
console.log('rooms', rooms);
|
||||
},
|
||||
onClose({socket, req}) {
|
||||
const {room: roomName} = req.query as any;
|
||||
const room = rooms.find(r => r.name === roomName);
|
||||
if (room) {
|
||||
room.sockets = room.sockets.filter(sock => sock !== socket);
|
||||
if (room.sockets.length === 0) {
|
||||
rooms = rooms.filter(r => r !== room);
|
||||
}
|
||||
}
|
||||
console.log('rooms', rooms);
|
||||
},
|
||||
onMessage({socket, payload}) {
|
||||
const {room: roomName} = payload as any;
|
||||
let room = rooms.find(r => r.name === roomName);
|
||||
if (!room) {
|
||||
console.log("creating room", roomName);
|
||||
room = {
|
||||
name: roomName,
|
||||
sockets: [],
|
||||
};
|
||||
rooms.push(room);
|
||||
}
|
||||
if (!room.sockets.includes(socket)) {
|
||||
console.log("adding socket to room", roomName);
|
||||
room.sockets.push(socket);
|
||||
}
|
||||
console.log("replying to everyone in room", roomName);
|
||||
room.sockets.forEach(sock => {
|
||||
sock.send(JSON.stringify(payload));
|
||||
});
|
||||
console.log('rooms', rooms);
|
||||
},
|
||||
} as const satisfies FirWebsocketHandler;
|
||||
|
||||
export default {
|
||||
method,
|
||||
url,
|
||||
payloadT,
|
||||
websocket,
|
||||
} as const satisfies FirRouteOptions<typeof payloadT>;
|
@ -111,7 +111,6 @@ export const getAuthorGames = async (where: {
|
||||
|
||||
export const insertRelease = async (props: {manifest: PicobookManifest, carts: {name: string; rom: number[]}[]}) => {
|
||||
const {manifest, carts} = props;
|
||||
// console.log('carts', JSON.stringify(carts));
|
||||
const {id: slug, author, repo, version} = manifest;
|
||||
const id = uuidv4();
|
||||
const now = new Date();
|
||||
|
43
src/server/file.ts
Normal file
43
src/server/file.ts
Normal file
@ -0,0 +1,43 @@
|
||||
// Import the framework and instantiate it
|
||||
import Fastify from 'fastify'
|
||||
import {fastifyWebsocket} from '@fastify/websocket';
|
||||
|
||||
const server = Fastify({
|
||||
logger: true
|
||||
});
|
||||
|
||||
server.register(fastifyWebsocket);
|
||||
|
||||
server.get("/api/ws/room", { websocket: true }, function wsHandler (socket) {
|
||||
console.log("Client connected!");
|
||||
|
||||
console.log(socket);
|
||||
|
||||
socket.on('upgrade', () => {
|
||||
console.log('upgraded');
|
||||
});
|
||||
|
||||
socket.on('message', message => {
|
||||
console.log('Message from server:', message);
|
||||
socket.send(`Echo: ${message}`);
|
||||
});
|
||||
|
||||
socket.on('close', () => {
|
||||
console.log('WebSocket connection closed');
|
||||
});
|
||||
|
||||
socket.on('error', error => {
|
||||
console.error('WebSocket error:', error);
|
||||
});
|
||||
});
|
||||
|
||||
// Run the server!
|
||||
try {
|
||||
// Note: host needs to be 0.0.0.0 rather than omitted or localhost, otherwise
|
||||
// it always returns an empty reply when used inside docker...
|
||||
// See: https://github.com/fastify/fastify/issues/935
|
||||
await server.listen({ port: parseInt(process.env["PORT"] ?? "3000"), host: "0.0.0.0" })
|
||||
} catch (err) {
|
||||
server.log.error(err)
|
||||
process.exit(1)
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
// Import the framework and instantiate it
|
||||
import Fastify from 'fastify'
|
||||
import fastifyStatic from '@fastify/static'
|
||||
import {fastifyWebsocket} from '@fastify/websocket';
|
||||
import { routeList } from "./routelist.ts";
|
||||
import { route } from "./util/routewrap.ts";
|
||||
import { attachRoute } from "./util/routewrap.ts";
|
||||
import { git } from './util/git.ts';
|
||||
import path from "path";
|
||||
import {fileURLToPath} from 'url';
|
||||
@ -17,13 +18,15 @@ const server = Fastify({
|
||||
logger: true
|
||||
});
|
||||
|
||||
server.register(fastifyWebsocket);
|
||||
|
||||
server.register(fastifyStatic, {
|
||||
root: new URL('public', import.meta.url).toString().slice("file://".length),
|
||||
prefix: '/',
|
||||
});
|
||||
|
||||
routeList.forEach(firRoute => {
|
||||
server.route(route(firRoute));
|
||||
attachRoute(server, firRoute);
|
||||
});
|
||||
|
||||
server.setNotFoundHandler((req, res) => {
|
||||
|
@ -2,6 +2,7 @@ import echo from "./api/echo.ts";
|
||||
import getAuthor from "./api/getAuthor.ts";
|
||||
import getRelease from "./api/getRelease.ts";
|
||||
import release from "./api/release.ts";
|
||||
import room from "./api/room.ts";
|
||||
import webhook from "./api/webhook.ts";
|
||||
|
||||
export const routeList = [
|
||||
@ -10,6 +11,7 @@ export const routeList = [
|
||||
release,
|
||||
getRelease,
|
||||
getAuthor,
|
||||
room,
|
||||
];
|
||||
|
||||
export type RouteList = typeof routeList;
|
@ -1,7 +1,10 @@
|
||||
import { Static, TSchema } from "@sinclair/typebox";
|
||||
import { Value } from "@sinclair/typebox/value";
|
||||
import { HTTPMethods } from "fastify"
|
||||
import { FastifyInstance, FastifyRequest, HTTPMethods } from "fastify"
|
||||
import { RouteOptions } from "fastify/types/route.js";
|
||||
import { type WebSocket } from "@fastify/websocket";
|
||||
|
||||
type WebsocketConnection = Parameters<Defined<RouteOptions["wsHandler"]>>[0];
|
||||
|
||||
type URLString = string;
|
||||
|
||||
@ -9,22 +12,94 @@ export type FirRouteInput<TPayloadSchema extends TSchema> = {
|
||||
payload: Static<TPayloadSchema>,
|
||||
}
|
||||
|
||||
export type FirWebsocketInput<TPayloadSchema extends TSchema> = {
|
||||
socket: WebsocketConnection,
|
||||
req: FastifyRequest,
|
||||
payload: Static<TPayloadSchema>,
|
||||
}
|
||||
|
||||
export type FirWebsocketHandler<TIn extends TSchema = TSchema> = {
|
||||
onMessage?(input: FirWebsocketInput<TIn>): void,
|
||||
onOpen?(input: {socket: WebSocket, req: FastifyRequest}): void,
|
||||
onClose?(input: {socket: WebSocket, req: FastifyRequest}): void,
|
||||
onError?(input: {socket: WebSocket, req: FastifyRequest, error: unknown}): void,
|
||||
};
|
||||
|
||||
export type FirRouteOptions<TIn extends TSchema = TSchema, TOut extends TSchema = TSchema> = {
|
||||
method: HTTPMethods,
|
||||
url: URLString,
|
||||
payloadT: TIn,
|
||||
responseT?: TOut,
|
||||
} & ({
|
||||
handler: (input: FirRouteInput<TIn>) => Static<TOut> | Promise<Static<TOut>>,
|
||||
}
|
||||
} | {
|
||||
websocket: FirWebsocketHandler<TIn>,
|
||||
})
|
||||
|
||||
export const route = <TIn extends TSchema, TOut extends TSchema>(routeOptions: FirRouteOptions<TIn, TOut>): RouteOptions => {
|
||||
type Defined<T> = T extends undefined ? never : T;
|
||||
|
||||
export const attachRoute = <TIn extends TSchema, TOut extends TSchema>(server: FastifyInstance, routeOptions: FirRouteOptions<TIn, TOut>) => {
|
||||
const {
|
||||
method,
|
||||
url,
|
||||
payloadT,
|
||||
handler,
|
||||
} = routeOptions;
|
||||
|
||||
if ("websocket" in routeOptions) {
|
||||
console.log('SETTING UP WS');
|
||||
const {websocket} = routeOptions;
|
||||
server.register(async function(fastify: FastifyInstance) {
|
||||
fastify.get('/api/ws/room', { websocket: true }, (socket: WebSocket, req: FastifyRequest) => {
|
||||
websocket.onOpen && websocket.onOpen({socket, req});
|
||||
socket.on('message', (message: any) => {
|
||||
const payload = JSON.parse(message.toString());
|
||||
if (Value.Check(payloadT, payload)) {
|
||||
websocket.onMessage && websocket.onMessage({socket, payload, req});
|
||||
} else {
|
||||
throw new Error("Payload wrong shape.");
|
||||
}
|
||||
});
|
||||
socket.on('close', () => {
|
||||
websocket.onClose && websocket.onClose({socket, req});
|
||||
});
|
||||
socket.on('error', (error: any) => {
|
||||
websocket.onError && websocket.onError({socket, error, req});
|
||||
});
|
||||
})
|
||||
});
|
||||
return;
|
||||
|
||||
// const {websocket} = routeOptions;
|
||||
// const augmentedWsHandler = (conn: Parameters<Defined<RouteOptions["wsHandler"]>>[0]) => {
|
||||
// console.log('HELLO');
|
||||
// conn.on("message", (message) => {
|
||||
// const payload = JSON.parse(message.toString());
|
||||
// if (Value.Check(payloadT, payload)) {
|
||||
// websocket({socket: conn, payload});
|
||||
// } else {
|
||||
// throw new Error("Payload wrong shape.");
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
// return {
|
||||
// method: 'GET', // WebSocket upgrades are GET requests
|
||||
// url,
|
||||
// // websocket: true,
|
||||
// wsHandler: augmentedWsHandler,
|
||||
// handler: (...args) => {
|
||||
// console.log('socket!');
|
||||
// const socket = args[0].socket.on("message", () => {
|
||||
// console.log("connected!");
|
||||
// })
|
||||
// },
|
||||
// // handler: (request, reply) => {
|
||||
// // reply.code(405).send({ message: 'Method Not Allowed' }); // Handle non-WebSocket requests
|
||||
// // }
|
||||
// }
|
||||
}
|
||||
|
||||
const {handler} = routeOptions;
|
||||
const augmentedHandler = (request: Parameters<RouteOptions["handler"]>[0]) => {
|
||||
const {
|
||||
body,
|
||||
@ -34,14 +109,13 @@ export const route = <TIn extends TSchema, TOut extends TSchema>(routeOptions: F
|
||||
if (Value.Check(payloadT, payload)) {
|
||||
return handler({payload});
|
||||
} else {
|
||||
|
||||
throw new Error("Payload wrong shape.");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
server.route({
|
||||
method,
|
||||
url,
|
||||
handler: augmentedHandler,
|
||||
}
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user