Last active
December 27, 2021 14:48
-
-
Save mfbx9da4/026ba8f2b60735001478bfcb6b3248c0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { serve } from 'https://deno.land/std/http/server.ts' | |
// import { assert, ErrorCode } from './assert.ts' | |
export enum ErrorCode { | |
RaceNotFound = 'RaceNotFound', | |
RaceMemberNotFound = 'RaceMemberNotFound', | |
RaceAlreadyExists = 'RaceAlreadyExists', | |
RaceMemberAlreadyExists = 'RaceMemberAlreadyExists', | |
AssertionError = 'AssertionError', | |
} | |
export type AssertionExtra = (Record<string, unknown> & { name?: ErrorCode }) | ErrorCode | |
export function assert(predicate: any, message: string, extra: AssertionExtra = {}): asserts predicate { | |
if (!predicate) { | |
extra = typeof extra === 'string' ? { name: extra } : extra | |
if (!('name' in extra)) { | |
extra.name = ErrorCode.AssertionError | |
} | |
throw new AssertionError(message, extra) | |
} | |
} | |
export class AssertionError extends Error { | |
constructor(message: string, extra: any = {}) { | |
super(message) | |
this.name = 'AssertionError' | |
Object.assign(this, extra) | |
} | |
} | |
const sockets = new Set<WebSocket>() | |
const channel = new BroadcastChannel('') | |
const headers = { 'Content-type': 'text/html' } | |
channel.onmessage = (e) => { | |
const { type, args } = e.data | |
switch (type) { | |
case 'notifyRaceMembers': | |
return notifyRaceMembersSockets(...(args as [string, any])) | |
} | |
} | |
function notifyRaceMembers(raceId: string, data: any) { | |
// notify sockets this instance knows about | |
notifyRaceMembersSockets(raceId, data) | |
// tell other deno instances to notify any of their connected sockets | |
channel.postMessage({ type: 'notifyRaceMembers', args: [raceId, data] }) | |
} | |
function notifyRaceMembersSockets(raceId: string, data: any) { | |
const members = raceMembers.get(raceId) || new Map() | |
for (const member of members.values()) { | |
member.socket?.send(JSON.stringify(data)) | |
} | |
} | |
type Race = { | |
raceId: string | |
createdAt: number | |
startAt: number | |
finishedAt?: number | |
winner?: string | |
codeSnippet: { | |
url: string | |
content: string | |
startIndex: number | |
} | |
heartbeatAt: number | |
} | |
type RaceMember = { | |
socket?: WebSocket | |
userId: string | |
raceId: string | |
name: string | |
score: number | |
heartbeatAt: number | |
} | |
const races: Map<string, Race> = new Map() | |
const raceMembers: Map<string, Map<string, RaceMember>> = new Map() | |
type User = { userId: string; name: string } | |
// plan | |
// keep all the races in memory | |
// whenever a race or race member is updated distribute accordingly | |
// createRace -> create entry in map and broadcast to all nodes | |
// joinRace -> create entry -> distribute - race conditions? better to have separate maps? | |
// ping -> update race and | |
// update score -> distribute user score until we find the machine with the socket | |
// -> tell all other sockets for that raceId | |
// on reconnect ? | |
type CreateRaceInput = { | |
user: User | |
raceId: string | |
startAt: number | |
} | |
function createRace(socket: WebSocket, data: unknown) { | |
// TODO: validate | |
const { user, raceId, startAt } = data as CreateRaceInput | |
assert(!races.get(raceId), 'Race already exists', ErrorCode.RaceAlreadyExists) | |
assert(!raceMembers.get(raceId), 'Race already exists', ErrorCode.RaceAlreadyExists) | |
const race: Race = { | |
raceId, | |
startAt, | |
createdAt: Date.now(), | |
heartbeatAt: Date.now(), | |
// TODO: fetch from github | |
codeSnippet: { | |
content: '', | |
startIndex: 0, | |
url: '', | |
}, | |
} | |
races.set(raceId, race) | |
raceMembers.set(raceId, new Map()) | |
raceMembers.get(raceId)!.set(user.userId, { | |
...user, | |
raceId: race.raceId, | |
socket, | |
score: 0, | |
heartbeatAt: Date.now(), | |
}) | |
notifyRaceMembers(raceId, race) | |
} | |
function getMembers(raceId: string) { | |
const members = raceMembers.get(raceId) | |
return members?.values() | |
} | |
type JoinRaceInput = { | |
user: User | |
raceId: string | |
} | |
function joinRace(socket: WebSocket, data: unknown) { | |
const { raceId, user } = data as JoinRaceInput | |
const race = races.get(raceId) | |
assert(race, 'Race not found', ErrorCode.RaceNotFound) | |
let members = raceMembers.get(raceId) | |
assert(members, 'Race not found', ErrorCode.RaceMemberNotFound) | |
const existingMember = members.get(raceId) | |
const raceMember: RaceMember = { | |
...existingMember, | |
...user, | |
raceId, | |
score: existingMember?.score || 0, | |
heartbeatAt: Date.now(), | |
socket, | |
} | |
members.set(raceId, raceMember) | |
const newMembers = raceMembers.get(raceId)!.values() | |
notifyRaceMembers(raceId, newMembers) | |
} | |
function pong(socket: WebSocket, _data: unknown) { | |
socket.send(JSON.stringify({ type: 'pong', t: Date.now() })) | |
} | |
await serve( | |
(r: Request) => { | |
try { | |
const { socket, response } = Deno.upgradeWebSocket(r) | |
sockets.add(socket) | |
socket.onmessage = (e) => { | |
const { type, data } = JSON.parse(e.data) | |
switch (type) { | |
case 'createRace': | |
return createRace(socket, data) | |
case 'joinRace': | |
return joinRace(socket, data) | |
case 'ping': | |
return pong(socket, data) | |
} | |
} | |
socket.onclose = (_) => sockets.delete(socket) | |
return response | |
} catch { | |
return new Response(html(), { headers }) | |
} | |
}, | |
{ port: 8080 }, | |
) | |
function html() { | |
return /* html */ ` | |
<script> | |
const protocol = new URL(location.href).protocol === 'http:' ? 'ws://' : 'wss://'; | |
const log = (str) => { | |
pre.textContent += str+"\\n" | |
} | |
let ws = new WebSocket(protocol+location.host) | |
ws.onmessage = e => log(e.data) | |
const ping = () => { | |
ws.send(JSON.stringify({ type: 'ping', t: Date.now() })) | |
log(JSON.stringify({ type: 'ping', t: Date.now() })) | |
setTimeout(ping, 1000) | |
} | |
ping() | |
</script> | |
<input onkeyup="event.key=='Enter'&&ws.send(this.value)"><pre id=pre>` | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment