Skip to content

Add multi-runtime support for Node.js, Bun, Deno, and Cloudflare Workers#2

Draft
Copilot wants to merge 9 commits intomainfrom
copilot/refactor-hocuspocus-server
Draft

Add multi-runtime support for Node.js, Bun, Deno, and Cloudflare Workers#2
Copilot wants to merge 9 commits intomainfrom
copilot/refactor-hocuspocus-server

Conversation

Copy link

Copilot AI commented Dec 31, 2025

Refactors @hocuspocus/server to support multiple JavaScript runtimes via runtime-specific entrypoints while maintaining 100% backward compatibility with existing Node.js usage.

Changes

Runtime Abstraction Layer

  • Core interfaces (src/core/interfaces.ts): WebSocketLike, RequestLike, RuntimeAdapter - normalize platform differences
  • Runtime-agnostic utilities: debounce, parameter parsing using Web Platform APIs

Runtime Adapters

  • src/adapters/node.ts - Node.js (wraps existing implementation)
  • src/adapters/bun.ts - Extends Node adapter
  • src/adapters/deno.ts - Web standard APIs
  • src/adapters/cloudflare-workers.ts - Durable Objects compatible

Cloudflare Workers Durable Object Support

  • BaseHocuspocusDurableObject (src/cloudflare/BaseHocuspocusDurableObject.ts) - Base class for Cloudflare Workers Durable Objects with hibernatable WebSocket API
    • Implements webSocketMessage, webSocketClose, and webSocketError methods
    • Wraps WebSockets with EventEmitter-compatible interface for Hocuspocus integration
    • Manages complete WebSocket lifecycle automatically
    • Provides simple inheritance model for users

Entrypoints & Packaging

  • Main entry unchanged: import { Server } from "@hocuspocus/server" works as before
  • Runtime-specific: @hocuspocus/server/{node,bun,deno,cloudflare-workers}
  • Conditional exports in package.json for automatic runtime detection
  • Build: Extended rollup config to generate separate bundles per runtime

Usage

Node.js (default):

import { Server } from "@hocuspocus/server";

Deno:

import { Hocuspocus } from "npm:@hocuspocus/server/deno";

Deno.serve({ port: 1234 }, (request) => {
  if (request.headers.get("upgrade") === "websocket") {
    const { socket, response } = Deno.upgradeWebSocket(request);
    hocuspocus.handleConnection(socket, request);
    return response;
  }
  return new Response("Hocuspocus Server");
});

Cloudflare Workers:

import { BaseHocuspocusDurableObject } from "@hocuspocus/server/cloudflare-workers";
import * as Y from "yjs";

export class HocuspocusDurableObject extends BaseHocuspocusDurableObject {
  constructor(state: DurableObjectState, env: Env) {
    super(state, env, {
      onLoadDocument: async ({ documentName }) => {
        const data = await this.ctx.storage.get(documentName);
        return data ? new Uint8Array(data as ArrayBuffer) : undefined;
      },
      onStoreDocument: async ({ documentName, document }) => {
        const state = Y.encodeStateAsUpdate(document);
        await this.ctx.storage.put(documentName, state);
      },
    });
  }
}

Architecture

Adapter pattern isolates runtime-specific behavior. Core Hocuspocus logic remains unchanged - adapters normalize WebSocket, timers, and request objects. Non-Node runtimes export Hocuspocus class directly (no Server wrapper which is Node HTTP-specific).

For Cloudflare Workers, BaseHocuspocusDurableObject provides 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

  • 4 core abstraction files (~600 LOC)
  • 4 runtime adapters (~220 LOC)
  • 4 runtime entrypoints (~130 LOC)
  • 1 Cloudflare Workers base class (~240 LOC)
  • Documentation: MULTI_RUNTIME.md, IMPLEMENTATION_SUMMARY.md

Backward Compatibility

Zero breaking changes. Existing imports, APIs, and behavior preserved for Node.js users.

Original prompt

Repository: jchoi2x/hocuspocus
Actor: GitHub agent / Copilot Workspace operating on this repo

Objective

Refactor the packages/server implementation of Hocuspocus so that:

  1. It supports multiple runtimes via runtime-specific entrypoints, selected using fields in package.json. Target runtimes:

    • Node.js
    • Bun
    • Deno
    • Cloudflare Workers (Durable Objects)
  2. The shared core logic of Hocuspocus (documents, connections, hooks, Yjs integration) is:

    • Runtime-agnostic, and
    • Based on Web Platform APIs where possible (Request, Response, URL, Headers, WebSocket or a small abstracted interface).
  3. Runtime-specific behavior is isolated behind small adapter entrypoints for each runtime, similar in spirit to how Hono works across environments.

  4. The package is wired so that different runtimes use different entrypoints via appropriate package.json fields (e.g. exports, browser, deno, or custom runtime fields as appropriate).

You should also:

  • Assess whether the build process needs changes to support this design, and if so, propose concrete modifications (e.g., multiple build outputs, conditional exports).
  • Implement as much of the refactor as is feasible in a single PR, with clear structure and documentation.

Context and references

  1. Current server implementation (this repo)

    Work primarily in:

    • packages/server/src/
      Especially:
      • Hocuspocus.ts
      • Server.ts
      • ClientConnection.ts
      • Connection.ts
      • Document.ts
      • DirectConnection.ts
      • IncomingMessage.ts
      • OutgoingMessage.ts
      • MessageReceiver.ts
      • types.ts
      • util/**
      • index.ts and any other entrypoints

    This is the existing Node-focused server implementation.

  2. 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.ts
    • packages/workers/hocuspocus/src/lib/hocuspocus/HocuspocusServer.ts
    • packages/workers/hocuspocus/src/lib/hocuspocus/ClientConnection.ts
    • packages/workers/hocuspocus/src/lib/hocuspocus/DirectConnection.ts
    • packages/workers/hocuspocus/src/lib/hocuspocus/Document.ts
    • packages/workers/hocuspocus/src/lib/hocuspocus/types.ts
    • Tests under packages/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 the turbo-workers implementation 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:

    • Contain all logic for:
      • Document creation/loading/storing
      • Yjs update handling
      • Awareness updates
      • Hooks and configuration (onConnect, onAuthenticate, onLoadDocument, onStoreDocument, onAwarenessUpdate, etc.)
    • Use Web Platform types wherever possible for inputs:
      • Request, Response
      • URL, URLSearchParams
      • Headers
      • A WebSocket-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):

    export interface RuntimeAdapter<Env = unknown> {
      // Handle an incoming HTTP/fetch-style request and dispatch to Hocuspocus core
      handleRequest(request: Request, env: Env, ctx?: unknown): Promise<Response>;
    
      // Optionally: attach to a WebSocket (if the runtime provides an explicit WS object)
      handleWebSocket?(socket: WebSocket, request: Request, env: Env, ctx?: unknown): Promise<void>;
    
      // Any strictly runtime-specific hooks (e.g., Durable Object alarms, Node timers, etc.) should live here.
    }
  • The Hocuspocus public API in this repo should become a thin wrapper that:

    • Configures the core,
    • Instantiates the appropriate adapter for the runtime, and
    • Exposes a clean entrypoint for each runtime (see next section).

2. Runtime-specific entrypoints and package.json mapping

Implement separate entrypoints for each target runtime, for example:

  • packages/server/src/entries/node.ts (or similar)
  • packages/server/src/entries/bun.ts
  • packages/server/src/entries/deno.ts
  • packages/server/src/entries/cloudflare-workers.ts (for Durable Objects)

Each entrypoint should:

  • Import the shared core and the runtime-specific adapter implementation.
  • Export a runtime-appropriate API, which might differ only slightly in bootstrap details, e....

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI self-assigned this Dec 31, 2025
Copilot AI and others added 7 commits December 31, 2025 21:31
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 {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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");
  }
}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented BaseHocuspocusDurableObject class that extends the Durable Object interface with hibernatable WebSocket support. The class:

  • Implements webSocketMessage, webSocketClose, and webSocketError methods
  • 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

Copilot AI changed the title [WIP] Refactor server implementation to support multiple runtimes Add multi-runtime support for Node.js, Bun, Deno, and Cloudflare Workers Dec 31, 2025
Copilot AI requested a review from jchoi2x December 31, 2025 21:50
…ebSocket support

Co-authored-by: jchoi2x <2028917+jchoi2x@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments