Skip to content

Instantly share code, notes, and snippets.

@mfbx9da4
Last active December 27, 2021 14:48
Show Gist options
  • Save mfbx9da4/026ba8f2b60735001478bfcb6b3248c0 to your computer and use it in GitHub Desktop.
Save mfbx9da4/026ba8f2b60735001478bfcb6b3248c0 to your computer and use it in GitHub Desktop.
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