From 0b2c0963c598d65535a3ef01ceeaf4c414d92edc Mon Sep 17 00:00:00 2001 From: Sunil Pai Date: Thu, 13 Jun 2024 21:39:43 +0100 Subject: [PATCH] start tidying up the DO, add some types to messages --- app/durableObjects/ChatRoom.server.ts | 68 +++++++++++++++------------ app/hooks/useBroadcastStatus.ts | 4 +- app/types/Env.ts | 2 +- package-lock.json | 31 ------------ package.json | 1 - 5 files changed, 40 insertions(+), 66 deletions(-) diff --git a/app/durableObjects/ChatRoom.server.ts b/app/durableObjects/ChatRoom.server.ts index 3e51b70a..854d2420 100644 --- a/app/durableObjects/ChatRoom.server.ts +++ b/app/durableObjects/ChatRoom.server.ts @@ -1,5 +1,10 @@ -import type { AppLoadContext } from '@remix-run/cloudflare' -import type { ClientMessage, ServerMessage, User } from '~/types/Messages' +import type { Env } from '~/types/Env' +import type { + ClientMessage, + MessageFromServer, + ServerMessage, + User, +} from '~/types/Messages' import { assertError } from '~/utils/assertError' import assertNever from '~/utils/assertNever' import { assertNonNullable } from '~/utils/assertNonNullable' @@ -23,32 +28,32 @@ type Session = { // ChatRoom implements a Durable Object that coordinates an individual chat room. Participants // connect to the room using WebSockets, and the room broadcasts messages from each participant // to all others. -export class ChatRoom { - storage: DurableObjectStorage - env: AppLoadContext - sessions: Session[] - lastTimestamp: number +export class ChatRoom implements DurableObject { + /** + * WebSocket objects for each client, along with some metadata + */ + sessions: Session[] = [] + /** + * We keep track of the last-seen message's timestamp just so that we can assign monotonically + increasing timestamps even if multiple messages arrive simultaneously (see below). There's + no need to store this to disk since we assume if the object is destroyed and recreated, much + more than a millisecond will have gone by. + */ + lastTimestamp: number = 0 stateSyncInterval: ReturnType | null = null - constructor(state: DurableObjectState, env: AppLoadContext) { - this.storage = state.storage - - // `env` is our environment bindings (discussed earlier). + // We'd like to use Cloudflare's fancy RPC Durable Objects + // but currently we can't figure out a way to mark cloudflare:workers + // as an external when remix compiles it. Until then, let's match the + // newer style member names .ctx and .env + ctx: DurableObjectState + env: Env + constructor(ctx: DurableObjectState, env: Env) { + this.ctx = ctx this.env = env - - // We will put the WebSocket objects for each client, along with some metadata, into - // `sessions`. - this.sessions = [] - - // We keep track of the last-seen message's timestamp just so that we can assign monotonically - // increasing timestamps even if multiple messages arrive simultaneously (see below). There's - // no need to store this to disk since we assume if the object is destroyed and recreated, much - // more than a millisecond will have gone by. - this.lastTimestamp = 0 - // check for previous sessions. - state.blockConcurrencyWhile(async () => { - this.sessions = (await this.storage.get('sessions')) ?? [] + this.ctx.blockConcurrencyWhile(async () => { + this.sessions = (await this.ctx.storage.get('sessions')) ?? [] this.sessions.forEach((s) => this.setupHeartbeatInterval(s)) }) } @@ -102,7 +107,7 @@ export class ChatRoom { from: 'server', timestamp: this.lastTimestamp, message, - }) + } satisfies MessageFromServer) ) session.messageQueue = session.messageQueue.filter((m) => m !== message) } else { @@ -111,8 +116,8 @@ export class ChatRoom { await this.storeSessions() } - storeSessions = async () => - this.storage.put( + async storeSessions() { + await this.ctx.storage.put( 'sessions', this.sessions.map( ({ @@ -122,10 +127,11 @@ export class ChatRoom { }) => s ) ) + } - broadcastState = () => { + broadcastState() { if (this.sessions.length > 0 && this.stateSyncInterval === null) { - this.stateSyncInterval = setInterval(this.broadcastState, 5000) + this.stateSyncInterval = setInterval(() => this.broadcastState(), 5000) } else if (this.sessions.length === 0 && this.stateSyncInterval !== null) { clearInterval(this.stateSyncInterval) this.stateSyncInterval = null @@ -140,7 +146,7 @@ export class ChatRoom { }) } - setupHeartbeatInterval = (session: Session) => { + setupHeartbeatInterval(session: Session) { const resetHeartBeatTimeout = () => { if (session.heartbeatTimeout) clearTimeout(session.heartbeatTimeout) session.heartbeatTimeout = setTimeout(() => { @@ -151,7 +157,7 @@ export class ChatRoom { return resetHeartBeatTimeout } - handleUserLeft = (session: Session) => { + handleUserLeft(session: Session) { session.quit = true this.sessions = this.sessions.filter((member) => member !== session) this.broadcastState() diff --git a/app/hooks/useBroadcastStatus.ts b/app/hooks/useBroadcastStatus.ts index aad42032..63ef8dff 100644 --- a/app/hooks/useBroadcastStatus.ts +++ b/app/hooks/useBroadcastStatus.ts @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { useUnmount } from 'react-use' -import type { User } from '~/types/Messages' +import type { ClientMessage, User } from '~/types/Messages' import type Peer from '~/utils/Peer.client' import type Signal from '~/utils/Signal' import type { RoomContextType } from './useRoomContext' @@ -80,7 +80,7 @@ export default function useBroadcastStatus({ transceiverSessionId: peer?.sessionId, tracks: {}, }, - }) + } satisfies ClientMessage) } }) } diff --git a/app/types/Env.ts b/app/types/Env.ts index 59c4ae5e..a9e8581e 100644 --- a/app/types/Env.ts +++ b/app/types/Env.ts @@ -8,7 +8,7 @@ export type Env = { TURN_SERVICE_TOKEN?: string TRACE_LINK?: string API_EXTRA_PARAMS?: string - limiters: DurableObjectNamespace + // limiters: DurableObjectNamespace rooms: DurableObjectNamespace MAX_WEBCAM_FRAMERATE?: string MAX_WEBCAM_BITRATE?: string diff --git a/package-lock.json b/package-lock.json index bf87ede6..ac36f508 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-visually-hidden": "^1.0.3", "@remix-run/cloudflare": "2.9.2", - "@remix-run/cloudflare-workers": "2.9.2", "@remix-run/react": "2.9.2", "@tensorflow-models/body-segmentation": "^1.0.2", "@tensorflow/tfjs-backend-webgl": "^4.15.0", @@ -3104,27 +3103,6 @@ } } }, - "node_modules/@remix-run/cloudflare-workers": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@remix-run/cloudflare-workers/-/cloudflare-workers-2.9.2.tgz", - "integrity": "sha512-strcxLSLhUmAtumj1AaAB7Azlz5kScLDJsF3fERH/aWBIK4UYBQvwkSmESP5PjekMSVFNlrlCcfPaykZng1JFQ==", - "dependencies": { - "@cloudflare/kv-asset-handler": "^0.1.3", - "@remix-run/cloudflare": "2.9.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "@cloudflare/workers-types": "^4.0.0", - "typescript": "^5.1.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/@remix-run/dev": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-2.9.2.tgz", @@ -18702,15 +18680,6 @@ "@remix-run/server-runtime": "2.9.2" } }, - "@remix-run/cloudflare-workers": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/@remix-run/cloudflare-workers/-/cloudflare-workers-2.9.2.tgz", - "integrity": "sha512-strcxLSLhUmAtumj1AaAB7Azlz5kScLDJsF3fERH/aWBIK4UYBQvwkSmESP5PjekMSVFNlrlCcfPaykZng1JFQ==", - "requires": { - "@cloudflare/kv-asset-handler": "^0.1.3", - "@remix-run/cloudflare": "2.9.2" - } - }, "@remix-run/dev": { "version": "2.9.2", "resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-2.9.2.tgz", diff --git a/package.json b/package.json index 5b07475a..419fb9e0 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "@radix-ui/react-tooltip": "^1.0.7", "@radix-ui/react-visually-hidden": "^1.0.3", "@remix-run/cloudflare": "2.9.2", - "@remix-run/cloudflare-workers": "2.9.2", "@remix-run/react": "2.9.2", "@tensorflow-models/body-segmentation": "^1.0.2", "@tensorflow/tfjs-backend-webgl": "^4.15.0",