Skip to content

Instantly share code, notes, and snippets.

@eduardvercaemer
Created April 6, 2024 19:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eduardvercaemer/16670bfc6760c4c92f7948ded04c3dbd to your computer and use it in GitHub Desktop.
Save eduardvercaemer/16670bfc6760c4c92f7948ded04c3dbd to your computer and use it in GitHub Desktop.
Basic websocket message passer for Cloudflare Durable Objects
declare global {
interface Env {
readonly CASTER: DurableObjectNamespace;
readonly RATE_LIMITER: {
limit(opt: { key: string }): Promise<{ success?: boolean }>;
};
}
}
// noinspection JSUnusedGlobalSymbols
export class Caster {
constructor(private readonly state: DurableObjectState) {
this.state.setWebSocketAutoResponse(
new WebSocketRequestResponsePair("ping", "pong"),
);
}
async fetch(request: Request) {
const url = new URL(request.url);
switch (request.method) {
case "GET": {
if (request.headers.get("upgrade") !== "websocket") {
return new Response(null, { status: 422 });
}
const tags = url.searchParams.getAll("t");
const pair = new WebSocketPair();
this.state.acceptWebSocket(
pair[1],
tags.length >= 0 ? tags : undefined,
);
return new Response(null, { status: 101, webSocket: pair[0] });
}
case "POST": {
if (
!request.headers.get("content-type")?.startsWith("application/json")
) {
return new Response(null, { status: 422 });
}
const tag = url.searchParams.get("t");
let payload: any;
try {
payload = await request.json();
} catch {
return new Response(null, { status: 400 });
}
const clients = this.state.getWebSockets(tag ?? undefined);
const message = JSON.stringify(payload);
clients.forEach((client) => client.send(message));
return new Response(null, { status: 201 });
}
default:
return new Response(null, { status: 422 });
}
}
}
// noinspection JSUnusedGlobalSymbols
export default {
async fetch(request: Request, env: Env) {
const url = new URL(request.url);
const uuid = url.searchParams.get("id");
if (!uuid) {
return new Response(null, { status: 403 });
}
const { success } = await env.RATE_LIMITER.limit({ key: uuid });
if (!success) {
return new Response(null, { status: 429 });
}
const id = env.CASTER.idFromName(uuid);
const caster = env.CASTER.get(id);
return caster.fetch(request);
},
};
name = "caster"
main = "index.ts"
compatibility_date = "2024-04-06"
[[unsafe.bindings]]
name = "RATE_LIMITER"
type = "ratelimit"
namespace_id = "1001"
simple = { limit = 100, period = 60 }
[[durable_objects.bindings]]
name = "CASTER"
class_name = "Caster"
[[migrations]]
tag = "v1"
new_classes = ["Caster"]
@eduardvercaemer
Copy link
Author

Basic websocket message passer.

# Connect
websocat 'wss://<worker>.worker.dev?id=foo'

# Connect with tags
websocat 'wss://<worker>.worker.dev?id=foo&t=user.create&t=user.delete'

# Post message
curlie 'https://<worker>.worker.dev?id=foo' value:=42
# Post message with tag
curlie 'https://<worker>.worker.dev?id=foo&t=user.delete' uid:=42

NOT MEANT FOR PUBLIC ROUTE. USE PROPER SERVICE BINDING OR ADD CUSTOM AUTH

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment