y-durableobjects

repository·main·Indexed 42 days ago

https://github.com/napolab/y-durableobjects

A library for implementing real-time, decentralized collaboration in Cloudflare Workers using Yjs and Durable Objects. It provides a scalable alternative to Node.js-based collaboration, integrating with Hono via the yRoute shorthand and offering JS RPC methods like getYDoc() and updateYDoc() to interact with YDoc state directly from Worker code.

Tokens
7K
Snippets
26
Records
34
Agent score
54%

What's inside y-durableobjects

  1. What is y-durableobjects?

    main

    The y-durableobjects project provides a way to integrate Yjs (a real-time collaboration framework) with Cloudflare Workers using Durable Objects.

    This architecture is designed to:

    • Eliminate the dependency on Node.js for real-time collaboration.
    • Provide a scalable and efficient solution inspired by the y-websocket adapter.
    • Enable real-time collaborative applications to run on the Cloudflare edge.
  2. Run the Yjs on Cloudflare Workers Demo

    main

    To run the full demonstration of Yjs integrated with Cloudflare Workers and Durable Objects locally, you must run both the UI application and the server emulator simultaneously.

    1. Setup and Installation

    First, clone the repository and install the dependencies in the example directory:

    git clone git@github.com:napolab/y-durableobjects.git
    cd y-durableobjects/example
    npm install

    2. Start the UI

    Navigate to the apps/web directory and start the development server:

    cd apps/web
    npm run dev

    3. Start the Server

    In a separate terminal, navigate to the apps/workers directory to emulate the Cloudflare Worker environment locally:

    cd apps/workers
    npm run dev
    # Installation
    git clone git@github.com:napolab/y-durableobjects.git
    cd y-durableobjects/example
    npm install
    
    # Run UI
    cd apps/web
    npm run dev
    
    # Run Server
    cd apps/workers
    npm run dev
  3. Configure type-aware ESLint rules for production

    main

    For production applications, it is recommended to enable type-aware lint rules in your ESLint configuration. This requires updating parserOptions to point to your tsconfig files and switching to type-checked plugin recommendations.

    Steps to upgrade:

    1. Update parserOptions to include project and tsconfigRootDir.
    2. Replace plugin:@typescript-eslint/recommended with plugin:@typescript-eslint/recommended-type-checked or plugin:@typescript-eslint/strict-type-checked.
    3. (Optional) Add plugin:@typescript-eslint/stylistic-type-checked.
    4. Install eslint-plugin-react and add plugin:react/recommended and plugin:react/jsx-runtime to the extends array.
    export default {
      // other rules...
      parserOptions: {
        ecmaVersion: 'latest',
        sourceType: 'module',
        project: ['./tsconfig.json', './tsconfig.node.json'],
        tsconfigRootDir: __dirname,
      },
    }
  4. Use Hono RPC on the ClientSide

    main

    To use typed WebSocket communications on the client, use Hono's hc client. This leverages the Hono WebSocket Helper, making the $ws method available.

    1. Server Setup

    Ensure your server exports the AppType (either via yRoute shorthand or manual implementation).

    2. Client Implementation

    import { hc } from "hono/client";
    import type { AppType } from "./server"; // Import the type from your server file
    
    const API_URL = "http://localhost:8787";
    
    // Create a typed client
    export const client = hc<AppType>(API_URL);
    
    // Access the WebSocket endpoint
    const ws = client.editor[":id"].$ws({ param: { id: "example" } });
    // ws is now a typed WebSocket
    import { hc } from "hono/client";
    import type { AppType } from "./server";
    
    const API_URL = "http://localhost:8787";
    export const client = hc<AppType>(API_URL);
    const ws = client.editor[":id"].$ws({ param: { id: "example" } });
  5. Configure Durable Objects in wrangler.toml

    main

    You must configure Durable Object bindings and migrations in your wrangler.toml file to allow your Worker to interact with y-durableobjects.

    Specifically, you need to define a binding with a class_name of YDurableObjects (or your custom subclass) and include a migration to create that class.

    Example wrangler.toml configuration:

    # Durable Objects binding
    [durable_objects]
    bindings = [
      { name = "Y_DURABLE_OBJECTS", class_name = "YDurableObjects" }
    ]
    
    # Durable Objects migrations
    [[migrations]]
    tag = "v1"
    new_classes = ["YDurableObjects"]
    [durable_objects]
    bindings = [
      { name = "Y_DURABLE_OBJECTS", class_name = "YDurableObjects" }
    ]
    
    [[migrations]]
    tag = "v1"
    new_classes = ["YDurableObjects"]
  6. Troubleshoot Cloudflare Env type collisions

    main

    If you see TypeScript errors where Cloudflare.Env is empty or undefined, it is likely because @cloudflare/workers-types (a peer dependency) is creating an empty interface that overwrites your custom environment types.

    Solution

    1. Generate fresh types: Use Wrangler to generate types based on your actual configuration.

      wrangler types
    2. Perform a clean install: If the issue persists, clear your dependency cache to remove residual @cloudflare/workers-types entries.

      # Delete lock files and node_modules
      rm -rf node_modules pnpm-lock.yaml
      # Reinstall dependencies
      pnpm install
    3. Verify: Run pnpm why @cloudflare/workers-types. It should return no results if the conflict is resolved.

    wrangler types
  7. Use y-durableobjects without Hono shorthand

    main

    If you need more control or are integrating Hono RPC manually, you can use the upgrade() helper. Note that due to current JS RPC limitations in Cloudflare, WebSocket connections must be handled via fetch.

    import { Hono } from "hono";
    import { YDurableObjects, type YDurableObjectsAppType } from "y-durableobjects";
    import { upgrade } from "y-durableobjects/helpers/upgrade";
    
    type Bindings = {
      Y_DURABLE_OBJECTS: DurableObjectNamespace<YDurableObjects<Env>>;
    };
    
    type Env = {
      Bindings: Bindings;
    };
    
    const app = new Hono<Env>();
    
    // Manually handle the upgrade to WebSocket
    app.get("/editor/:id", upgrade(), async (c) => {
      const id = c.env.Y_DURABLE_OBJECTS.idFromName(c.req.param("id"));
      const stub = c.env.Y_DURABLE_OBJECTS.get(id);
    
      const url = new URL("/", c.req.url);
      const client = hc<YDurableObjectsAppType>(url.toString(), {
        fetch: stub.fetch.bind(stub),
      });
    
      const res = await client.rooms[":id"].$get(
        { param: { id: c.req.param("id") } },
        { init: { headers: c.req.raw.headers } },
      );
    
      return new Response(null, {
        webSocket: res.webSocket,
        status: res.status,
        statusText: res.statusText,
      });
    });
    
    export default app;
    export type AppType = typeof app;
    export { YDurableObjects };
    import { Hono } from "hono";
    import { YDurableObjects, type YDurableObjectsAppType } from "y-durableobjects";
    import { upgrade } from "y-durableobjects/helpers/upgrade";
    
    type Bindings = {
      Y_DURABLE_OBJECTS: DurableObjectNamespace<YDurableObjects<Env>>;
    };
    
    type Env = {
      Bindings: Bindings;
    };
    
    const app = new Hono<Env>();
    app.get("/editor/:id", upgrade(), async (c) => {
      const id = c.env.Y_DURABLE_OBJECTS.idFromName(c.req.param("id"));
      const stub = c.env.Y_DURABLE_OBJECTS.get(id);
    
      const url = new URL("/", c.req.url);
      const client = hc<YDurableObjectsAppType>(url.toString(), {
        fetch: stub.fetch.bind(stub),
      });
    
      const res = await client.rooms[":id"].$get(
        { param: { id: c.req.param("id") } },
        { init: { headers: c.req.raw.headers } },
      );
    
      return new Response(null, {
        webSocket: res.webSocket,
        status: res.status,
        statusText: res.statusText,
      });
    });
    
    export default app;
    export type AppType = typeof app;
    export { YDurableObjects };
  8. Use y-durableobjects with Hono shorthand

    main

    The easiest way to integrate y-durableobjects with Hono is using the yRoute shorthand. This automatically handles the routing for real-time collaboration.

    import { Hono } from "hono";
    import { YDurableObjects, yRoute } from "y-durableobjects";
    
    type Bindings = {
      Y_DURABLE_OBJECTS: DurableObjectNamespace<YDurableObjects<Env>>;
    };
    
    type Env = {
      Bindings: Bindings;
    };
    
    const app = new Hono<Env>();
    
    // The yRoute helper sets up the collaboration endpoint
    const route = app.route(
      "/editor",
      yRoute<Env>((env) => env.Y_DURABLE_OBJECTS),
    );
    
    export default route;
    export type AppType = typeof route;
    export { YDurableObjects };
    import { Hono } from "hono";
    import { YDurableObjects, yRoute } from "y-durableobjects";
    
    type Bindings = {
      Y_DURABLE_OBJECTS: DurableObjectNamespace<YDurableObjects<Env>>;
    };
    
    type Env = {
      Bindings: Bindings;
    };
    
    const app = new Hono<Env>();
    
    const route = app.route(
      "/editor",
      yRoute<Env>((env) => env.Y_DURABLE_OBJECTS),
    );
    
    export default route;
    export type AppType = typeof route;
    export { YDurableObjects };
  9. Extend YDurableObjects with custom methods

    main

    You can extend the YDurableObjects class to implement custom logic and expose new methods via JS RPC. You have access to the internal this.doc (the YDoc instance).

    import { YDurableObjects } from "y-durableobjects";
    
    export class CustomDurableObject extends YDurableObjects {
      async customMethod() {
        // Access and manipulate the YDoc state directly
        const update = new Uint8Array([
          /* some update data */
        ]);
        this.doc.update(update);
        
        // You can also call cleanup manually if needed
        await this.cleanup();
      }
    }
    import { YDurableObjects } from "y-durableobjects";
    
    export class CustomDurableObject extends YDurableObjects {
      async customMethod() {
        const update = new Uint8Array([/* ... */]);
        this.doc.update(update);
        await this.cleanup();
      }
    }