Add multi-runtime support for Node.js, Bun, Deno, and Cloudflare Workers#2
Add multi-runtime support for Node.js, Bun, Deno, and Cloudflare Workers#2
Conversation
Co-authored-by: jchoi2x <2028917+jchoi2x@users.noreply.github.com>
Co-authored-by: jchoi2x <2028917+jchoi2x@users.noreply.github.com>
Co-authored-by: jchoi2x <2028917+jchoi2x@users.noreply.github.com>
Co-authored-by: jchoi2x <2028917+jchoi2x@users.noreply.github.com>
Co-authored-by: jchoi2x <2028917+jchoi2x@users.noreply.github.com>
Co-authored-by: jchoi2x <2028917+jchoi2x@users.noreply.github.com>
Co-authored-by: jchoi2x <2028917+jchoi2x@users.noreply.github.com>
| /** | ||
| * Cloudflare Workers runtime adapter implementation | ||
| */ | ||
| export class CloudflareWorkersRuntimeAdapter implements RuntimeAdapter { |
There was a problem hiding this comment.
@copilot
If this is what get imported when imported from cloudflare, it needs to be a DurableObject instance that utilizes the hibernatable websocket api. The websocket interface that it passes down should extend EventEmitter so that from webSocketMessage, webSocketClose, webSocketError methods of the durable object we can call ws.emit appropriately.
This needs to be adapted. The code from jchoi2x/turbo-repo has a BaseHocuspocus class that's a durable object and instantiates HocuspocusServer
export class WebSocketHibernationServer extends DurableObject {
async fetch(request: Request): Promise<Response> {
// Creates two ends of a WebSocket connection.
const webSocketPair = new WebSocketPair();
const [client, server] = Object.values(webSocketPair);
// Calling `acceptWebSocket()` connects the WebSocket to the Durable Object, allowing the WebSocket to send and receive messages.
// Unlike `ws.accept()`, `state.acceptWebSocket(ws)` allows the Durable Object to be hibernated
// When the Durable Object receives a message during Hibernation, it will run the `constructor` to be re-initialized
this.ctx.acceptWebSocket(server);
return new Response(null, {
status: 101,
webSocket: client,
});
}
async webSocketMessage(ws: WebSocket, message: ArrayBuffer | string) {
// Upon receiving a message from the client, reply with the same message,
// but will prefix the message with "[Durable Object]: " and return the number of connections.
ws.send(
`[Durable Object] message: ${message}, connections: ${this.ctx.getWebSockets().length}`,
);
}
async webSocketClose(
ws: WebSocket,
code: number,
reason: string,
wasClean: boolean,
) {
// If the client closes the connection, the runtime will invoke the webSocketClose() handler.
ws.close(code, "Durable Object is closing WebSocket");
}
}
There was a problem hiding this comment.
Implemented BaseHocuspocusDurableObject class that extends the Durable Object interface with hibernatable WebSocket support. The class:
- Implements
webSocketMessage,webSocketClose, andwebSocketErrormethods - Wraps WebSockets with an EventEmitter-compatible interface (supports
on,emit,once,off) - Handles WebSocket lifecycle automatically via the hibernatable API
- Provides simple inheritance model - users just extend the base class and pass configuration to
super()
Usage is now simplified to:
export class HocuspocusDurableObject extends BaseHocuspocusDurableObject {
constructor(state: DurableObjectState, env: Env) {
super(state, env, { /* config */ });
}
}Fixed in commit 55331c8
…ebSocket support Co-authored-by: jchoi2x <2028917+jchoi2x@users.noreply.github.com>
Refactors
@hocuspocus/serverto support multiple JavaScript runtimes via runtime-specific entrypoints while maintaining 100% backward compatibility with existing Node.js usage.Changes
Runtime Abstraction Layer
src/core/interfaces.ts):WebSocketLike,RequestLike,RuntimeAdapter- normalize platform differencesRuntime Adapters
src/adapters/node.ts- Node.js (wraps existing implementation)src/adapters/bun.ts- Extends Node adaptersrc/adapters/deno.ts- Web standard APIssrc/adapters/cloudflare-workers.ts- Durable Objects compatibleCloudflare Workers Durable Object Support
src/cloudflare/BaseHocuspocusDurableObject.ts) - Base class for Cloudflare Workers Durable Objects with hibernatable WebSocket APIwebSocketMessage,webSocketClose, andwebSocketErrormethodsEntrypoints & Packaging
import { Server } from "@hocuspocus/server"works as before@hocuspocus/server/{node,bun,deno,cloudflare-workers}package.jsonfor automatic runtime detectionUsage
Node.js (default):
Deno:
Cloudflare Workers:
Architecture
Adapter pattern isolates runtime-specific behavior. Core Hocuspocus logic remains unchanged - adapters normalize WebSocket, timers, and request objects. Non-Node runtimes export
Hocuspocusclass directly (noServerwrapper which is Node HTTP-specific).For Cloudflare Workers,
BaseHocuspocusDurableObjectprovides a complete Durable Object implementation with hibernatable WebSocket support, handling all WebSocket lifecycle events and providing an EventEmitter-compatible interface for seamless Hocuspocus integration.Files Added
MULTI_RUNTIME.md,IMPLEMENTATION_SUMMARY.mdBackward Compatibility
Zero breaking changes. Existing imports, APIs, and behavior preserved for Node.js users.
Original prompt
Repository:
jchoi2x/hocuspocusActor: GitHub agent / Copilot Workspace operating on this repo
Objective
Refactor the
packages/serverimplementation of Hocuspocus so that:It supports multiple runtimes via runtime-specific entrypoints, selected using fields in
package.json. Target runtimes:The shared core logic of Hocuspocus (documents, connections, hooks, Yjs integration) is:
Request,Response,URL,Headers,WebSocketor a small abstracted interface).Runtime-specific behavior is isolated behind small adapter entrypoints for each runtime, similar in spirit to how Hono works across environments.
The package is wired so that different runtimes use different entrypoints via appropriate
package.jsonfields (e.g.exports,browser,deno, or custom runtime fields as appropriate).You should also:
Context and references
Current server implementation (this repo)
Work primarily in:
packages/server/src/Especially:
Hocuspocus.tsServer.tsClientConnection.tsConnection.tsDocument.tsDirectConnection.tsIncomingMessage.tsOutgoingMessage.tsMessageReceiver.tstypes.tsutil/**index.tsand any other entrypointsThis is the existing Node-focused server implementation.
Workers/Durable Objects reference implementation (other repo)
There is a Cloudflare Workers / Durable Objects port in a separate (private) repo the same user maintains:
jchoi2x/turbo-workers, at:packages/workers/hocuspocus/src/lib/hocuspocus/BaseHocuspocus.tspackages/workers/hocuspocus/src/lib/hocuspocus/HocuspocusServer.tspackages/workers/hocuspocus/src/lib/hocuspocus/ClientConnection.tspackages/workers/hocuspocus/src/lib/hocuspocus/DirectConnection.tspackages/workers/hocuspocus/src/lib/hocuspocus/Document.tspackages/workers/hocuspocus/src/lib/hocuspocus/types.tspackages/workers/hocuspocus/src/tests/**You may read that repo for reference (the current user has access) but all actual work and changes must be done in
jchoi2x/hocuspocus. Use theturbo-workersimplementation to understand how Hocuspocus is adapted to Cloudflare Workers/Durable Objects and to inform your design of runtime adapters.Requirements
1. Design a core + adapter architecture
Extract or define a runtime-agnostic core module (or set of modules), for example under a new folder such as:
packages/server/src/core/This core should:
onConnect,onAuthenticate,onLoadDocument,onStoreDocument,onAwarenessUpdate, etc.)Request,ResponseURL,URLSearchParamsHeadersWebSocket-like abstraction (you can define an interface if you need to support Node/Bun/Deno/Workers differences).Define a small runtime adapter interface, for example (name and exact shape are up to you):
The Hocuspocus public API in this repo should become a thin wrapper that:
2. Runtime-specific entrypoints and
package.jsonmappingImplement separate entrypoints for each target runtime, for example:
packages/server/src/entries/node.ts(or similar)packages/server/src/entries/bun.tspackages/server/src/entries/deno.tspackages/server/src/entries/cloudflare-workers.ts(for Durable Objects)Each entrypoint should:
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.