Skip to content

Commit e8cd169

Browse files
committed
feat: add websocket server to the backend to publish vote count updates
1 parent 965e8a6 commit e8cd169

4 files changed

Lines changed: 74 additions & 3 deletions

File tree

backend/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
"deploy": "wrangler deploy --minify",
66
"cf-typegen": "wrangler types --env-interface CloudflareBindings",
77
"db:generate": "drizzle-kit generate",
8-
"db:migrate": "wrangler d1 migrations apply --env=production poll",
9-
"db:migrate:local": "wrangler d1 migrations apply --local poll",
108
"format": "prettier --write .",
119
"format:check": "prettier --check ."
1210
},

backend/src/index.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,30 @@ const createContext = (env: Env, request: Request): Context => ({
130130
// Export default function to handle requests
131131
export default {
132132
async fetch(request: Request, env: Env): Promise<Response> {
133+
const url = new URL(request.url);
134+
135+
// Handle WebSocket upgrade requests
136+
if (url.pathname.endsWith("/websocket")) {
137+
const upgradeHeader = request.headers.get("Upgrade");
138+
139+
if (!upgradeHeader || upgradeHeader !== "websocket") {
140+
return new Response("Durable Object expected Upgrade: websocket", {
141+
status: 426,
142+
});
143+
}
144+
145+
const pollId = url.searchParams.get("pollId");
146+
147+
if (!pollId) {
148+
return new Response("Poll-Id header is required", { status: 400 });
149+
}
150+
151+
const durableObjectId = env.POLL_DURABLE_OBJECT.idFromName(pollId);
152+
const stub = env.POLL_DURABLE_OBJECT.get(durableObjectId);
153+
154+
return stub.fetch(request);
155+
}
156+
133157
// Handle CORS preflight requests
134158
if (request.method === "OPTIONS") {
135159
return new Response(null, {

backend/src/poll-durable-object.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,26 @@ import { Env } from ".";
66
import migrations from "../drizzle/migrations";
77
import { Result } from "./result";
88
import { poll, pollOptions, votes } from "./schema";
9-
import { PollWithOptions, PollWithResults, Vote, VoteInsert } from "./types";
9+
import {
10+
PollWithOptions,
11+
PollWithResults,
12+
Vote,
13+
VoteInsert,
14+
WebSocketMessage,
15+
} from "./types";
1016

1117
export class PollDurableObject extends DurableObject {
1218
storage: DurableObjectStorage;
1319
db: DrizzleSqliteDODatabase<any>;
1420

21+
currentlyConnectedWebSockets: WebSocket[];
22+
1523
constructor(ctx: DurableObjectState, env: Env) {
1624
super(ctx, env);
1725

1826
this.storage = this.ctx.storage;
1927
this.db = drizzle(this.storage, { logger: false });
28+
this.currentlyConnectedWebSockets = [];
2029

2130
ctx.blockConcurrencyWhile(async () => {
2231
await this._migrate();
@@ -146,12 +155,47 @@ export class PollDurableObject extends DurableObject {
146155
.values(voteInserts)
147156
.returning();
148157

158+
this.currentlyConnectedWebSockets.forEach((ws) => {
159+
if (ws.readyState === WebSocket.OPEN) {
160+
ws.send(
161+
JSON.stringify({
162+
type: "newVote",
163+
data: insertedVotes,
164+
} satisfies WebSocketMessage<Vote[]>)
165+
);
166+
}
167+
});
168+
149169
return {
150170
success: true,
151171
data: insertedVotes,
152172
};
153173
}
154174

175+
async fetch(request: Request): Promise<Response> {
176+
// Creates two ends of a WebSocket connection.
177+
const webSocketPair = new WebSocketPair();
178+
const [client, server] = Object.values(webSocketPair);
179+
180+
// Calling `accept()` tells the runtime that this WebSocket is to begin terminating
181+
// request within the Durable Object. It has the effect of "accepting" the connection,
182+
// and allowing the WebSocket to send and receive messages.
183+
this.ctx.acceptWebSocket(server);
184+
this.currentlyConnectedWebSockets.push(client);
185+
186+
// If the client closes the connection, the runtime will close the connection too.
187+
server.addEventListener("close", (cls: CloseEvent) => {
188+
this.currentlyConnectedWebSockets =
189+
this.currentlyConnectedWebSockets.filter((ws) => ws !== client);
190+
server.close(cls.code, "Durable Object is closing WebSocket");
191+
});
192+
193+
return new Response(null, {
194+
status: 101,
195+
webSocket: client,
196+
});
197+
}
198+
155199
async _migrate() {
156200
migrate(this.db, migrations);
157201
}

backend/src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { poll, pollOptions, votes } from "./schema";
22

3+
export type WebSocketMessage<T> = {
4+
type: "newVote";
5+
data: T;
6+
};
7+
38
export type Poll = typeof poll.$inferSelect;
49
export type PollInsert = typeof poll.$inferInsert;
510
export type PollOption = typeof pollOptions.$inferSelect;

0 commit comments

Comments
 (0)