Skip to content

Commit c0b6bc9

Browse files
committed
Add hibernation websocket handler for cloudflare DO
1 parent 3c124aa commit c0b6bc9

File tree

6 files changed

+153
-2
lines changed

6 files changed

+153
-2
lines changed

.changeset/nervous-birds-fetch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@triplit/server': patch
3+
---
4+
5+
Add hibernation websocket handler for cloudflare DO
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { DurableObject } from 'cloudflare:workers';
2+
import {
3+
upgradeWebSocket,
4+
upgradeWebSocketHibernation,
5+
} from '@triplit/server/cloudflare';
6+
import { createTriplitHonoServer } from '@triplit/server/hono';
7+
import { CloudflareDurableObjectKVStore } from '@triplit/db/storage/cf-durable-object';
8+
9+
type ExtendedDurableObjectState = DurableObjectState & {
10+
honoWs?: {
11+
events?: any;
12+
wsContext?: any;
13+
};
14+
};
15+
16+
export class MyDurableObject extends DurableObject {
17+
state: ExtendedDurableObjectState;
18+
private appPromise: Promise<
19+
Awaited<ReturnType<typeof createTriplitHonoServer>>
20+
>;
21+
22+
constructor(ctx: ExtendedDurableObjectState, env: Env) {
23+
super(ctx, env);
24+
console.log('CALLING CONSTRUCTOR');
25+
this.state = ctx;
26+
// Create the Triplit server
27+
this.appPromise = createTriplitHonoServer(
28+
{
29+
// add any configuration options here
30+
jwtSecret: env.JWT_SECRET,
31+
// this is the Triplit storage provider for Durable Objects
32+
storage: new CloudflareDurableObjectKVStore(this.state.storage),
33+
},
34+
// inject the platform-specific WebSocket upgrade function
35+
upgradeWebSocketHibernation(ctx)
36+
);
37+
}
38+
39+
async fetch(request: Request) {
40+
// Await the app initialization before handling the request
41+
const app = await this.appPromise;
42+
return app.fetch(request);
43+
}
44+
45+
async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
46+
await this.appPromise;
47+
if (this.state.honoWs?.events?.onMessage) {
48+
const wsContext = this.state.honoWs.wsContext;
49+
this.state.honoWs.events.onMessage(
50+
{
51+
data: message,
52+
},
53+
wsContext
54+
);
55+
}
56+
}
57+
58+
async webSocketClose(
59+
ws: WebSocket,
60+
code: number,
61+
reason: string,
62+
wasClean: boolean
63+
) {
64+
await this.appPromise;
65+
if (this.state.honoWs?.events?.onClose) {
66+
const wsContext = this.state.honoWs.wsContext;
67+
this.state.honoWs.events.onClose(
68+
{
69+
code,
70+
reason,
71+
wasClean,
72+
},
73+
wsContext
74+
);
75+
}
76+
}
77+
78+
async webSocketError(ws: WebSocket, error: Error) {
79+
await this.appPromise;
80+
if (this.state.honoWs?.events?.onError) {
81+
const wsContext = this.state.honoWs.wsContext;
82+
this.state.honoWs.events.onError(error, wsContext);
83+
}
84+
}
85+
}
86+
87+
export default {
88+
async fetch(request, env, _ctx): Promise<Response> {
89+
// Get the Durable Object ID (this is where you could easily add multi-tenancy)
90+
let id: DurableObjectId = env.MY_DURABLE_OBJECT.idFromName('triplitDB');
91+
let stub = env.MY_DURABLE_OBJECT.get(id);
92+
93+
// Forward the request to the Durable Object
94+
return await stub.fetch(request);
95+
},
96+
} satisfies ExportedHandler<Env>;

packages/cf-worker-server/worker-configuration.d.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
// Generated by Wrangler by running `wrangler types --include-runtime=false` (hash: 4c81d17e8de07f650cddf73ce751ba8d)
22
declare namespace Cloudflare {
33
interface Env {
4-
MY_DURABLE_OBJECT: DurableObjectNamespace<import('./src/index').MyDurableObject>;
4+
MY_DURABLE_OBJECT: DurableObjectNamespace<
5+
import('./src/standard').MyDurableObject
6+
>;
57
JWT_SECRET: string;
68
}
79
}

packages/cf-worker-server/wrangler.jsonc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
{
66
"$schema": "node_modules/wrangler/config-schema.json",
77
"name": "cf-worker-server",
8-
"main": "src/index.ts",
8+
"main": "src/standard.ts",
99
"compatibility_date": "2025-04-30",
1010
"migrations": [
1111
{

packages/server/src/cloudflare.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,51 @@ export const upgradeWebSocket: UpgradeWebSocket<
6969
webSocket: client,
7070
});
7171
});
72+
73+
export function upgradeWebSocketHibernation(
74+
ctx: any
75+
): UpgradeWebSocket<WebSocket, any, Omit<WSEvents<WebSocket>, 'onOpen'>> {
76+
return defineWebSocketHelper(async (c, events) => {
77+
const upgradeHeader = c.req.header('Upgrade');
78+
if (upgradeHeader !== 'websocket') {
79+
return;
80+
}
81+
82+
// @ts-expect-error WebSocketPair is not typed
83+
const webSocketPair = new WebSocketPair();
84+
const client: any = webSocketPair[0];
85+
const server: any = webSocketPair[1];
86+
87+
const wsContext = new WSContext<WebSocket>({
88+
close: (code, reason) => server.close(code, reason),
89+
get protocol() {
90+
return server.protocol;
91+
},
92+
raw: server,
93+
get readyState() {
94+
return server.readyState as WSReadyState;
95+
},
96+
url: server.url ? new URL(server.url) : null,
97+
send: (source) => server.send(source),
98+
});
99+
100+
// Add hono content so its accessible by the durable object after initialization
101+
ctx.honoWs = {
102+
events,
103+
wsContext,
104+
};
105+
// This API allows the durable object to accept the WebSocket connection
106+
ctx.acceptWebSocket(server);
107+
108+
// note: cloudflare workers doesn't support 'open' event
109+
if (events.onOpen) {
110+
events.onOpen(new Event('open'), wsContext);
111+
}
112+
113+
return new Response(null, {
114+
status: 101,
115+
// @ts-expect-error - webSocket is not typed
116+
webSocket: client,
117+
});
118+
});
119+
}

0 commit comments

Comments
 (0)