Skip to content

Commit

Permalink
Merge pull request #24 from sor4chi/feat/hibernation-websocket-example
Browse files Browse the repository at this point in the history
feat: Hibernation Websocket API example
  • Loading branch information
sor4chi authored Nov 30, 2023
2 parents d6dc068 + bd4e559 commit da19b88
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 0 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 8 additions & 0 deletions examples/hibernatable-chat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
```
npm install
npm run dev
```

```
npm run deploy
```
21 changes: 21 additions & 0 deletions examples/hibernatable-chat/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
58 changes: 58 additions & 0 deletions examples/hibernatable-chat/src/chat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
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);
});
}

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(vars.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();
state.acceptWebSocket(server);

server.serializeAttachment({ clientId });

return new Response(null, { status: 101, webSocket: client });
}
});

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();
}
});
});
22 changes: 22 additions & 0 deletions examples/hibernatable-chat/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
88 changes: 88 additions & 0 deletions examples/hibernatable-chat/src/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
export const Template = /*html*/ `
<!DOCTYPE html>
<body>
<input type="text" id="text_input" /><br/>
<button id="send_button">Send</button> <br/>
<div id="output_div"></div>
<script type="text/javascript">
let currentWebSocket = null;
const hostname = window.location.host;
const protocol = window.location.protocol;
const wsProtocol = protocol === "https:" ? "wss:" : "ws:";
const outputDiv = document.getElementById('output_div');
const sendButton = document.getElementById('send_button');
const textInput = document.getElementById('text_input');
async function getMessages() {
const res = await fetch(protocol + "//" + hostname + "/chat/messages");
const messages = await res.json();
return messages;
}
function insertMessage(message) {
const span = document.createElement("span");
span.innerText = message.timestamp + ": ";
const p = document.createElement("p");
p.innerText = message.text;
p.prepend(span);
outputDiv.appendChild(p);
}
window.onload = async () => {
const messages = await getMessages();
messages.forEach(insertMessage);
}
function join() {
const ws = new WebSocket(wsProtocol + "//" + hostname + "/chat/websocket");
let rejoined = false;
const startTime = Date.now();
ws.addEventListener("open", event => {
currentWebSocket = ws;
});
ws.addEventListener("message", event => {
insertMessage(JSON.parse(event.data));
});
ws.addEventListener("close", event => {
console.log("WebSocket closed, reconnecting:", event.code, event.reason);
rejoin();
});
ws.addEventListener("error", event => {
console.log("WebSocket error, reconnecting:", event);
rejoin();
});
const rejoin = async () => {
if (!rejoined) {
rejoined = true;
currentWebSocket = null;
let timeSinceLastJoin = Date.now() - startTime;
if (timeSinceLastJoin < 5000) {
await new Promise(resolve => setTimeout(resolve, 5000 - timeSinceLastJoin));
}
join();
}
}
}
sendButton.addEventListener("click", event => {
const text = textInput.value;
const now = new Date().toLocaleString("en-US", { hour: "numeric", minute: "numeric", hour12: true });
const message = { text, timestamp: now };
insertMessage(message);
currentWebSocket.send(JSON.stringify(message));
textInput.value = "";
});
join();
</script>
</body>
</html>
`.trim();
17 changes: 17 additions & 0 deletions examples/hibernatable-chat/tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
},
}
9 changes: 9 additions & 0 deletions examples/hibernatable-chat/wrangler.toml
Original file line number Diff line number Diff line change
@@ -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"]
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit da19b88

Please sign in to comment.