From c16c2565b1b8f804d17561ef883dcbb140a12bb3 Mon Sep 17 00:00:00 2001 From: sor4chi Date: Fri, 1 Dec 2023 00:02:21 +0900 Subject: [PATCH 1/3] chore: copy Chat example as Hibernatable Chat example --- examples/hibernatable-chat/README.md | 8 ++ examples/hibernatable-chat/package.json | 21 ++++++ examples/hibernatable-chat/src/chat.ts | 56 ++++++++++++++ examples/hibernatable-chat/src/index.ts | 22 ++++++ examples/hibernatable-chat/src/template.ts | 88 ++++++++++++++++++++++ examples/hibernatable-chat/tsconfig.json | 17 +++++ examples/hibernatable-chat/wrangler.toml | 9 +++ pnpm-lock.yaml | 16 ++++ 8 files changed, 237 insertions(+) create mode 100644 examples/hibernatable-chat/README.md create mode 100644 examples/hibernatable-chat/package.json create mode 100644 examples/hibernatable-chat/src/chat.ts create mode 100644 examples/hibernatable-chat/src/index.ts create mode 100644 examples/hibernatable-chat/src/template.ts create mode 100644 examples/hibernatable-chat/tsconfig.json create mode 100644 examples/hibernatable-chat/wrangler.toml diff --git a/examples/hibernatable-chat/README.md b/examples/hibernatable-chat/README.md new file mode 100644 index 0000000..cc58e96 --- /dev/null +++ b/examples/hibernatable-chat/README.md @@ -0,0 +1,8 @@ +``` +npm install +npm run dev +``` + +``` +npm run deploy +``` diff --git a/examples/hibernatable-chat/package.json b/examples/hibernatable-chat/package.json new file mode 100644 index 0000000..20c1717 --- /dev/null +++ b/examples/hibernatable-chat/package.json @@ -0,0 +1,21 @@ +{ + "name": "hono-do-example-hibernatable-chat", + "private": true, + "version": "0.0.0", + "scripts": { + "lint": "eslint --fix --ext .ts,.tsx src", + "lint:check": "eslint --ext .ts,.tsx src", + "format": "prettier --write \"src/**/*.{ts,tsx}\"", + "format:check": "prettier --check \"src/**/*.{ts,tsx}\"", + "dev": "wrangler dev src/index.ts", + "deploy": "wrangler deploy --minify src/index.ts" + }, + "dependencies": { + "hono": "^3.6.0", + "hono-do": "workspace:*" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20230821.0", + "wrangler": "^3.7.0" + } +} diff --git a/examples/hibernatable-chat/src/chat.ts b/examples/hibernatable-chat/src/chat.ts new file mode 100644 index 0000000..d12221f --- /dev/null +++ b/examples/hibernatable-chat/src/chat.ts @@ -0,0 +1,56 @@ +import { generateHonoObject } from "hono-do"; + +function uuidv4() { + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { + const r = (Math.random() * 16) | 0, + v = c == "x" ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +} + +export const Chat = generateHonoObject("/chat", (app) => { + const messages: { + timestamp: string; + text: string; + }[] = []; + const sessions = new Map(); + + app.get("/messages", async (c) => c.json(messages)); + + app.get("/websocket", async (c) => { + if (c.req.header("Upgrade") === "websocket") { + return await handleWebSocketUpgrade(); + } + return c.text("Not found", 404); + }); + + async function handleWebSocketUpgrade() { + const [client, server] = Object.values(new WebSocketPair()); + const clientId = uuidv4(); + server.accept(); + + sessions.set(clientId, server); + + server.addEventListener("message", (msg) => { + if (typeof msg.data !== "string") return; + messages.push(JSON.parse(msg.data)); + broadcast(msg.data, clientId); + }); + + return new Response(null, { status: 101, webSocket: client }); + } + + function broadcast(message: string, senderClientId?: string) { + for (const [clientId, webSocket] of sessions.entries()) { + if (clientId === senderClientId) { + continue; + } + + try { + webSocket.send(message); + } catch (error) { + sessions.delete(clientId); + } + } + } +}); diff --git a/examples/hibernatable-chat/src/index.ts b/examples/hibernatable-chat/src/index.ts new file mode 100644 index 0000000..77e72d6 --- /dev/null +++ b/examples/hibernatable-chat/src/index.ts @@ -0,0 +1,22 @@ +import { Hono } from "hono"; + +import { Template } from "./template"; + +const app = new Hono<{ + Bindings: { + CHAT: DurableObjectNamespace; + }; +}>(); + +app.get("/", (c) => { + return c.html(Template); +}); + +app.all("/chat/*", (c) => { + const id = c.env.CHAT.idFromName("chat"); + const obj = c.env.CHAT.get(id); + return obj.fetch(c.req.raw); +}); + +export default app; +export * from "./chat"; diff --git a/examples/hibernatable-chat/src/template.ts b/examples/hibernatable-chat/src/template.ts new file mode 100644 index 0000000..01beb3c --- /dev/null +++ b/examples/hibernatable-chat/src/template.ts @@ -0,0 +1,88 @@ +export const Template = /*html*/ ` + + +
+
+
+ + + +`.trim(); diff --git a/examples/hibernatable-chat/tsconfig.json b/examples/hibernatable-chat/tsconfig.json new file mode 100644 index 0000000..9cd8489 --- /dev/null +++ b/examples/hibernatable-chat/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "lib": [ + "esnext" + ], + "types": [ + "@cloudflare/workers-types" + ], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + }, +} \ No newline at end of file diff --git a/examples/hibernatable-chat/wrangler.toml b/examples/hibernatable-chat/wrangler.toml new file mode 100644 index 0000000..f7b30ba --- /dev/null +++ b/examples/hibernatable-chat/wrangler.toml @@ -0,0 +1,9 @@ +name = "chat" +compatibility_date = "2023-01-01" + +[durable_objects] +bindings = [{ name = "CHAT", class_name = "Chat" }] + +[[migrations]] +tag = "v1" +new_classes = ["Chat"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3aea457..f3bee4e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,6 +115,22 @@ importers: specifier: ^3.7.0 version: 3.8.0 + examples/hibernatable-chat: + dependencies: + hono: + specifier: ^3.6.0 + version: 3.6.0 + hono-do: + specifier: workspace:* + version: link:../../packages/hono-do + devDependencies: + '@cloudflare/workers-types': + specifier: ^4.20230821.0 + version: 4.20230904.0 + wrangler: + specifier: ^3.7.0 + version: 3.8.0 + packages/hono-do: dependencies: hono: From eb768e013c3e6f23c22aa346fbccbb0f605a0ff5 Mon Sep 17 00:00:00 2001 From: sor4chi Date: Fri, 1 Dec 2023 00:12:59 +0900 Subject: [PATCH 2/3] fix: rewrite the example using Hibernatiable Websocket API --- examples/hibernatable-chat/src/chat.ts | 56 +++++++++++++------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/examples/hibernatable-chat/src/chat.ts b/examples/hibernatable-chat/src/chat.ts index d12221f..7505d44 100644 --- a/examples/hibernatable-chat/src/chat.ts +++ b/examples/hibernatable-chat/src/chat.ts @@ -8,14 +8,19 @@ function uuidv4() { }); } -export const Chat = generateHonoObject("/chat", (app) => { - const messages: { - timestamp: string; - text: string; - }[] = []; - const sessions = new Map(); +declare module "hono-do" { + interface HonoObjectVars { + messages: { + timestamp: string; + text: string; + }[]; + } +} + +export const Chat = generateHonoObject("/chat", (app, state, vars) => { + vars.messages = []; - app.get("/messages", async (c) => c.json(messages)); + app.get("/messages", async (c) => c.json(vars.messages)); app.get("/websocket", async (c) => { if (c.req.header("Upgrade") === "websocket") { @@ -27,30 +32,27 @@ export const Chat = generateHonoObject("/chat", (app) => { async function handleWebSocketUpgrade() { const [client, server] = Object.values(new WebSocketPair()); const clientId = uuidv4(); - server.accept(); + state.acceptWebSocket(server); - sessions.set(clientId, server); - - server.addEventListener("message", (msg) => { - if (typeof msg.data !== "string") return; - messages.push(JSON.parse(msg.data)); - broadcast(msg.data, clientId); - }); + server.serializeAttachment({ clientId }); return new Response(null, { status: 101, webSocket: client }); } +}); - function broadcast(message: string, senderClientId?: string) { - for (const [clientId, webSocket] of sessions.entries()) { - if (clientId === senderClientId) { - continue; - } - - try { - webSocket.send(message); - } catch (error) { - sessions.delete(clientId); - } +Chat.webSocketMessage(async (webSocket, msg, state, vars) => { + const { clientId: senderClientId } = await webSocket.deserializeAttachment(); + state.getWebSockets().forEach((ws) => { + const { clientId } = ws.deserializeAttachment(); + if (clientId === senderClientId) { + return; } - } + + try { + vars.messages.push(JSON.parse(msg.toString())); + ws.send(msg.toString()); + } catch (error) { + ws.close(); + } + }); }); From bd4e5592b436138a1ab7f10f788385b961f8dde5 Mon Sep 17 00:00:00 2001 From: sor4chi Date: Fri, 1 Dec 2023 00:16:12 +0900 Subject: [PATCH 3/3] chore: update README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 29d8820..3304dde 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,11 @@ export const Counter = generateHonoObject("/counter", async (app, state) => { You want to find more? Check out the [examples](./examples)! +## Support + +- [x] [`alarm` API](https://developers.cloudflare.com/durable-objects/api/alarms/) +- [x] [`Hibernation Websocket API`](https://developers.cloudflare.com/durable-objects/learning/websockets/#websocket-hibernation) + ## License [MIT](./LICENSE)