From c498e9263422480bf26c4dbadc53c6ee5e7d2166 Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Wed, 3 Jul 2024 09:48:54 +0100 Subject: [PATCH] partyserver and friends - use `nanoid` for id generation. This lets us control the generated id lengths without sacrificing entropy, and avoids special characters like `-`. - use `partysocket` as a websocket replacement, that comes inbuilt with buffering, resilience, reconnection, etc - separates `mode` and `env` in `AppLoadContext` - avoid a roundtrip when generating a new room - rewrite the durable object with partyserver - enables hibernation - responds to pings without waking up the object - use the connection to store session data about the user - use an alarm to broadcast roomstate every 30 seconds --- app/api/roomsApi.server.ts | 89 - app/components/MuteUserButton.tsx | 12 +- app/components/Toast.tsx | 4 +- app/durableObjects/ChatRoom.server.ts | 431 +- app/hooks/useBroadcastStatus.ts | 73 +- app/hooks/useRoom.ts | 98 +- app/hooks/useSignal.ts | 25 - app/root.test.ts | 26 +- app/root.tsx | 2 +- app/routes/_index.tsx | 18 +- app/routes/_room.$roomName.room.tsx | 15 +- app/routes/_room.tsx | 20 +- app/routes/api.bugReport.tsx | 6 +- app/routes/api.calls.$.tsx | 4 +- app/routes/api.deadTrack.tsx | 4 +- app/routes/api.room.$roomName.$.tsx | 36 - app/routes/new.tsx | 7 +- app/routes/parties.rooms.$roomName.$.tsx | 19 + app/types/AppLoadContext.ts | 3 +- app/types/Messages.ts | 15 +- app/utils/Signal.ts | 165 - app/utils/getIceServers.server.ts | 4 +- package-lock.json | 4649 +++++++--------------- package.json | 6 +- public/mockServiceWorker.js | 478 +-- server.ts | 6 +- 26 files changed, 2068 insertions(+), 4147 deletions(-) delete mode 100644 app/api/roomsApi.server.ts delete mode 100644 app/hooks/useSignal.ts delete mode 100644 app/routes/api.room.$roomName.$.tsx create mode 100644 app/routes/parties.rooms.$roomName.$.tsx delete mode 100644 app/utils/Signal.ts diff --git a/app/api/roomsApi.server.ts b/app/api/roomsApi.server.ts deleted file mode 100644 index 0a333d0..0000000 --- a/app/api/roomsApi.server.ts +++ /dev/null @@ -1,89 +0,0 @@ -import type { AppLoadContext } from '@remix-run/cloudflare' - -export async function handleApiRequest( - path: string[], - request: Request, - env: AppLoadContext -) { - // We've received at API request. Route the request based on the path. - - switch (path[0]) { - case 'room': { - // Request for `/api/room/...`. - - if (!path[1]) { - // The request is for just "/api/room", with no ID. - if (request.method == 'POST') { - // POST to /api/room creates a private room. - // - // Incidentally, this code doesn't actually store anything. It just generates a valid - // unique ID for this namespace. Each durable object namespace has its own ID space, but - // IDs from one namespace are not valid for any other. - // - // The IDs returned by `newUniqueId()` are unguessable, so are a valid way to implement - // "anyone with the link can access" sharing. Additionally, IDs generated this way have - // a performance benefit over IDs generated from names: When a unique ID is generated, - // the system knows it is unique without having to communicate with the rest of the - // world -- i.e., there is no way that someone in the UK and someone in New Zealand - // could coincidentally create the same ID at the same time, because unique IDs are, - // well, unique! - let id = env.rooms.newUniqueId() - return new Response(id.toString(), { - headers: { 'Access-Control-Allow-Origin': '*' }, - }) - } else { - // If we wanted to support returning a list of public rooms, this might be a place to do - // it. The list of room names might be a good thing to store in KV, though a singleton - // Durable Object is also a possibility as long as the Cache API is used to cache reads. - // (A caching layer would be needed because a single Durable Object is single-threaded, - // so the amount of traffic it can handle is limited. Also, caching would improve latency - // for users who don't happen to be located close to the singleton.) - // - // For this demo, though, we're not implementing a public room list, mainly because - // inevitably some trolls would probably register a bunch of offensive room names. Sigh. - return new Response('Method not allowed', { status: 405 }) - } - } - - // OK, the request is for `/api/room//...`. It's time to route to the Durable Object - // for the specific room. - let name = path[1] - - // Each Durable Object has a 256-bit unique ID. IDs can be derived from string names, or - // chosen randomly by the system. - let id - if (name.match(/^[0-9a-f]{64}$/)) { - // The name is 64 hex digits, so let's assume it actually just encodes an ID. We use this - // for private rooms. `idFromString()` simply parses the text as a hex encoding of the raw - // ID (and verifies that this is a valid ID for this namespace). - id = env.rooms.idFromString(name) - } else if (name.length <= 32) { - // Treat as a string room name (limited to 32 characters). `idFromName()` consistently - // derives an ID from a string. - id = env.rooms.idFromName(name) - } else { - return new Response('Name too long', { status: 404 }) - } - - // Get the Durable Object stub for this room! The stub is a client object that can be used - // to send messages to the remote Durable Object instance. The stub is returned immediately; - // there is no need to await it. This is important because you would not want to wait for - // a network round trip before you could start sending requests. Since Durable Objects are - // created on-demand when the ID is first used, there's nothing to wait for anyway; we know - // an object will be available somewhere to receive our requests. - let roomObject = env.rooms.get(id) - - // Compute a new URL with `/api/room/` removed. We'll forward the rest of the path - // to the Durable Object. - let newUrl = new URL(request.url) - newUrl.pathname = '/' + path.slice(2).join('/') - - // Send the request to the object. The `fetch()` method of a Durable Object stub has the - // same signature as the global `fetch()` function, but the request is always sent to the - // object, regardless of the request's URL. - return roomObject.fetch(newUrl.toString(), request) - } - default: - return new Response('Not found', { status: 404 }) - } -} diff --git a/app/components/MuteUserButton.tsx b/app/components/MuteUserButton.tsx index 7022a12..4a6c2be 100644 --- a/app/components/MuteUserButton.tsx +++ b/app/components/MuteUserButton.tsx @@ -1,7 +1,7 @@ import type { FC } from 'react' import { useRoomContext } from '~/hooks/useRoomContext' import { useUserMetadata } from '~/hooks/useUserMetadata' -import type { User } from '~/types/Messages' +import type { ClientMessage, User } from '~/types/Messages' import AlertDialog from './AlertDialog' import type { ButtonProps } from './Button' import { Button } from './Button' @@ -62,10 +62,12 @@ export const MuteUserButton: FC = ({