Yuki Takei 2 недель назад
Родитель
Сommit
dd9907be88

+ 1 - 1
.kiro/specs/migrate-to-y-websocket/spec.json

@@ -3,7 +3,7 @@
   "created_at": "2026-03-19T00:00:00.000Z",
   "updated_at": "2026-03-19T00:00:00.000Z",
   "language": "en",
-  "phase": "tasks-approved",
+  "phase": "implementation-complete",
   "approvals": {
     "requirements": {
       "generated": true,

+ 20 - 20
.kiro/specs/migrate-to-y-websocket/tasks.md

@@ -1,13 +1,13 @@
 # Implementation Plan
 
-- [ ] 1. Add y-websocket dependency and adapt persistence layer
-- [ ] 1.1 (P) Add y-websocket package to apps/app and packages/editor
+- [x] 1. Add y-websocket dependency and adapt persistence layer
+- [x] 1.1 (P) Add y-websocket package to apps/app and packages/editor
   - Add `y-websocket@^2.0.4` to both `apps/app/package.json` and `packages/editor/package.json`
   - Classify as `dependencies` in apps/app (server-side `bin/utils` is used at runtime) and verify Turbopack externalisation after build
   - Run `pnpm install` to update lockfile
   - _Requirements: 2.1, 8.3_
 
-- [ ] 1.2 (P) Adapt the MongoDB persistence layer to the y-websocket persistence interface
+- [x] 1.2 (P) Adapt the MongoDB persistence layer to the y-websocket persistence interface
   - Update `create-mongodb-persistence.ts` to return an object matching y-websocket's `setPersistence` shape (`bindState`, `writeState`, `provider`)
   - The `bindState` implementation extends the current logic: load persisted Y.Doc, compute diff, store update, apply persisted state, register incremental update handler with `updatedAt` metadata
   - After applying persisted state within `bindState`, determine `YDocStatus` and call `syncYDoc` to synchronize with the latest revision — this guarantees correct ordering (persistence load completes before sync runs)
@@ -17,8 +17,8 @@
   - Update the `Persistence` type import from y-websocket's `bin/utils` instead of y-socket.io
   - _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 6.3, 5.2, 5.3_
 
-- [ ] 2. Implement WebSocket upgrade authentication handler
-- [ ] 2.1 Create the upgrade handler that authenticates WebSocket connections using session cookies
+- [x] 2. Implement WebSocket upgrade authentication handler
+- [x] 2.1 Create the upgrade handler that authenticates WebSocket connections using session cookies
   - Parse the `cookie` header from the HTTP upgrade request to extract the session ID
   - Load the session from the session store (Redis or MongoDB, matching GROWI's express-session configuration)
   - Deserialize the user from the session via passport's `deserializeUser`
@@ -29,8 +29,8 @@
   - Attach the authenticated user to the request object for downstream use
   - _Requirements: 3.1, 3.2, 3.3, 3.4_
 
-- [ ] 3. Rewrite YjsService to use y-websocket server utilities
-- [ ] 3.1 Replace YSocketIO with ws.WebSocketServer and y-websocket document management
+- [x] 3. Rewrite YjsService to use y-websocket server utilities
+- [x] 3.1 Replace YSocketIO with ws.WebSocketServer and y-websocket document management
   - Change the constructor to accept both `httpServer` and `io` (instead of only `io`)
   - Create a `WebSocket.Server` with `noServer: true` mode
   - Call y-websocket's `setPersistence` with the adapted persistence layer from task 1.2
@@ -38,22 +38,22 @@
   - Ensure Socket.IO's upgrade handling for `/socket.io/` is not affected by checking the URL path before intercepting
   - _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 2.1, 2.3_
 
-- [ ] 3.2 Integrate document status API and force-sync
+- [x] 3.2 Integrate document status API and force-sync
   - Replace `ysocketio.documents.get(pageId)` with y-websocket's `docs.get(pageId)` for `getCurrentYdoc` and `syncWithTheLatestRevisionForce`
   - Preserve all public API behavior of `IYjsService` (getYDocStatus, getCurrentYdoc, syncWithTheLatestRevisionForce)
   - Update `sync-ydoc.ts` type imports: change `Document` from y-socket.io to y-websocket's `WSSharedDoc` (or `Y.Doc`)
   - Note: sync-on-load (`syncYDoc`) and awareness bridging are handled inside `bindState` of the PersistenceAdapter (task 1.2), not via `setContentInitializor`
   - _Requirements: 6.1, 6.2, 6.3, 6.4_
 
-- [ ] 4. Update server initialization flow
-- [ ] 4.1 Pass httpServer to YjsService initialization
+- [x] 4. Update server initialization flow
+- [x] 4.1 Pass httpServer to YjsService initialization
   - Update `initializeYjsService` to accept both `httpServer` and `io` parameters
   - Update the call site in `crowi/index.ts` to pass `httpServer` alongside `socketIoService.io`
   - Verify the initialization order: httpServer created → Socket.IO attached → YjsService initialized with both references
   - _Requirements: 2.3_
 
-- [ ] 5. Migrate client-side provider to WebsocketProvider
-- [ ] 5.1 (P) Replace SocketIOProvider with WebsocketProvider in the collaborative editor hook
+- [x] 5. Migrate client-side provider to WebsocketProvider
+- [x] 5.1 (P) Replace SocketIOProvider with WebsocketProvider in the collaborative editor hook
   - Change the import from `y-socket.io` to `y-websocket`
   - Construct the WebSocket URL dynamically: use `wss://` when the page is served over HTTPS, `ws://` otherwise, appending `/yjs` as the base path
   - Use `pageId` as the `roomname` parameter (same as current)
@@ -63,36 +63,36 @@
   - Lifecycle cleanup remains identical: `provider.disconnect()`, `provider.destroy()`
   - _Requirements: 2.2, 2.4, 5.1, 5.4_
 
-- [ ] 6. Update Vite dev server configuration
-- [ ] 6.1 (P) Configure the packages/editor Vite dev server to use y-websocket
+- [x] 6. Update Vite dev server configuration
+- [x] 6.1 (P) Configure the packages/editor Vite dev server to use y-websocket
   - Replace the `YSocketIO` import with y-websocket server utilities (`setupWSConnection`, `setPersistence`)
   - Create a `WebSocket.Server` with `noServer: true` in Vite's `configureServer` hook
   - Handle WebSocket upgrade events on the dev server's `httpServer` for the `/yjs/` path prefix
   - Ensure the Vite HMR WebSocket and the Yjs WebSocket do not conflict (different paths)
   - _Requirements: 7.1, 7.2_
 
-- [ ] 7. Remove y-socket.io and finalize dependencies
-- [ ] 7.1 Remove all y-socket.io references from the codebase
+- [x] 7. Remove y-socket.io and finalize dependencies
+- [x] 7.1 Remove all y-socket.io references from the codebase
   - Remove `y-socket.io` from `apps/app/package.json` and `packages/editor/package.json`
   - Verify no remaining imports or type references to `y-socket.io` modules across the monorepo
   - Run `pnpm install` to update the lockfile
   - Verify `y-websocket` is classified correctly (`dependencies` vs `devDependencies`) by checking Turbopack externalisation: run `turbo run build --filter @growi/app` and check `apps/app/.next/node_modules/` for y-websocket
   - _Requirements: 8.1, 8.2, 8.3_
 
-- [ ] 8. Integration and concurrency tests
-- [ ] 8.1 Add integration tests for the WebSocket connection and sync flow
+- [x] 8. Integration and concurrency tests
+- [x] 8.1 Add integration tests for the WebSocket connection and sync flow
   - Test the full connection flow: WebSocket upgrade → authentication → document creation → sync step 1/2
   - Test multi-client sync: two clients connect to the same page, verify both receive each other's edits via the same server-side Y.Doc
   - Test reconnection: client disconnects and reconnects, verify it receives updates that occurred during disconnection
   - Test persistence round-trip: document persisted when all clients disconnect, state restored when a new client connects
   - _Requirements: 1.3, 1.4, 4.3, 4.5_
 
-- [ ] 8.2 Add concurrency tests for document initialization safety
+- [x] 8.2 Add concurrency tests for document initialization safety
   - Test simultaneous connections: multiple clients connect to the same page at the exact same time, verify that exactly one Y.Doc instance exists on the server (the core race condition fix)
   - Test disconnect-during-connect: one client disconnects while another is connecting, verify no document corruption or data loss
   - _Requirements: 1.1, 1.2, 1.5_
 
-- [ ] 8.3 Add unit tests for the upgrade authentication handler
+- [x] 8.3 Add unit tests for the upgrade authentication handler
   - Test valid session cookie → user deserialized → page access granted → upgrade proceeds
   - Test expired/invalid session → 401 response → socket destroyed
   - Test valid user but no page access → 403 response → socket destroyed

+ 1 - 1
apps/app/package.json

@@ -290,7 +290,7 @@
     "xss": "^1.0.15",
     "y-codemirror.next": "^0.3.5",
     "y-mongodb-provider": "^0.2.0",
-    "y-socket.io": "^1.1.3",
+    "y-websocket": "^2.0.4",
     "yjs": "^13.6.18",
     "zod": "^3.24.2"
   },

+ 5 - 1
apps/app/src/server/crowi/index.ts

@@ -588,7 +588,11 @@ class Crowi {
     this.socketIoService.attachServer(httpServer);
 
     // Initialization YjsService
-    initializeYjsService(this.socketIoService.io);
+    initializeYjsService(
+      httpServer,
+      this.socketIoService.io,
+      this.sessionConfig,
+    );
 
     await this.autoInstall();
 

+ 54 - 14
apps/app/src/server/service/yjs/create-mongodb-persistence.ts

@@ -1,23 +1,38 @@
-import type { Persistence } from 'y-socket.io/dist/server';
+import { YDocStatus } from '@growi/core/dist/consts';
+import type { Server } from 'socket.io';
+import type { WSSharedDoc, YWebsocketPersistence } from 'y-websocket/bin/utils';
 import * as Y from 'yjs';
 
+import { SocketEventName } from '~/interfaces/websocket';
+import {
+  getRoomNameWithId,
+  RoomPrefix,
+} from '~/server/service/socket-io/helper';
 import loggerFactory from '~/utils/logger';
 
 import type { MongodbPersistence } from './extended/mongodb-persistence';
+import type { syncYDoc as syncYDocType } from './sync-ydoc';
 
 const logger = loggerFactory('growi:service:yjs:create-mongodb-persistence');
 
+type GetYDocStatus = (pageId: string) => Promise<YDocStatus>;
+
 /**
- * Based on the example by https://github.com/MaxNoetzold/y-mongodb-provider?tab=readme-ov-file#an-other-example
- * @param mdb
- * @returns
+ * Creates a y-websocket compatible persistence layer backed by MongoDB.
+ *
+ * bindState also handles:
+ * - sync-on-load (syncYDoc) after persisted state is applied
+ * - awareness event bridge to Socket.IO rooms
  */
 export const createMongoDBPersistence = (
   mdb: MongodbPersistence,
-): Persistence => {
-  const persistece: Persistence = {
+  io: Server,
+  syncYDoc: typeof syncYDocType,
+  getYDocStatus: GetYDocStatus,
+): YWebsocketPersistence => {
+  const persistence: YWebsocketPersistence = {
     provider: mdb,
-    bindState: async (docName, ydoc) => {
+    bindState: async (docName: string, ydoc: WSSharedDoc) => {
       logger.debug('bindState', { docName });
 
       const persistedYdoc = await mdb.getYDoc(docName);
@@ -40,23 +55,48 @@ export const createMongoDBPersistence = (
       // send the persisted data to clients
       Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(persistedYdoc));
 
+      // cleanup some memory
+      persistedYdoc.destroy();
+
+      // sync with the latest revision after persisted state is applied
+      const ydocStatus = await getYDocStatus(docName);
+      syncYDoc(mdb, ydoc, { ydocStatus });
+
       // store updates of the document in db
-      ydoc.on('update', async (update) => {
+      ydoc.on('update', (update: Uint8Array) => {
         mdb.storeUpdate(docName, update);
         mdb.setTypedMeta(docName, 'updatedAt', Date.now());
       });
 
-      // cleanup some memory
-      persistedYdoc.destroy();
+      // register awareness event bridge to Socket.IO rooms
+      ydoc.awareness.on('update', async () => {
+        const pageId = docName;
+        const awarenessStateSize = ydoc.awareness.getStates().size;
+
+        io.in(getRoomNameWithId(RoomPrefix.PAGE, pageId)).emit(
+          SocketEventName.YjsAwarenessStateSizeUpdated,
+          awarenessStateSize,
+        );
+
+        // emit draft status when last user leaves
+        if (awarenessStateSize === 0) {
+          const status = await getYDocStatus(pageId);
+          const hasYdocsNewerThanLatestRevision =
+            status === YDocStatus.DRAFT || status === YDocStatus.ISOLATED;
+
+          io.in(getRoomNameWithId(RoomPrefix.PAGE, pageId)).emit(
+            SocketEventName.YjsHasYdocsNewerThanLatestRevisionUpdated,
+            hasYdocsNewerThanLatestRevision,
+          );
+        }
+      });
     },
-    writeState: async (docName) => {
+    writeState: async (docName: string) => {
       logger.debug('writeState', { docName });
-      // This is called when all connections to the document are closed.
-
       // flush document on close to have the smallest possible database
       await mdb.flushDocument(docName);
     },
   };
 
-  return persistece;
+  return persistence;
 };

+ 2 - 2
apps/app/src/server/service/yjs/sync-ydoc.ts

@@ -1,6 +1,6 @@
 import { Origin, YDocStatus } from '@growi/core';
 import type { Delta } from '@growi/editor';
-import type { Document } from 'y-socket.io/dist/server';
+import type { WSSharedDoc } from 'y-websocket/bin/utils';
 
 import loggerFactory from '~/utils/logger';
 
@@ -22,7 +22,7 @@ type Context = {
  */
 export const syncYDoc = async (
   mdb: MongodbPersistence,
-  doc: Document,
+  doc: WSSharedDoc,
   context: true | Context,
 ): Promise<void> => {
   const pageId = doc.name;

+ 147 - 0
apps/app/src/server/service/yjs/upgrade-handler.spec.ts

@@ -0,0 +1,147 @@
+import type { IncomingMessage } from 'node:http';
+import type { Duplex } from 'node:stream';
+import { mock } from 'vitest-mock-extended';
+
+import { createUpgradeHandler } from './upgrade-handler';
+
+vi.mock('mongoose', () => {
+  const isAccessiblePageByViewer = vi.fn();
+  return {
+    default: {
+      model: () => ({ isAccessiblePageByViewer }),
+    },
+    __mockIsAccessible: isAccessiblePageByViewer,
+  };
+});
+
+vi.mock('express-session', () => ({
+  default: () => (_req: any, _res: any, next: () => void) => next(),
+}));
+
+vi.mock('passport', () => ({
+  default: {
+    initialize: () => (_req: any, _res: any, next: () => void) => next(),
+    session: () => (_req: any, _res: any, next: () => void) => next(),
+  },
+}));
+
+const getIsAccessibleMock = async () => {
+  const mod = await import('mongoose');
+  return (mod as any).__mockIsAccessible as ReturnType<typeof vi.fn>;
+};
+
+const sessionConfig = {
+  rolling: true,
+  secret: 'test-secret',
+  resave: false,
+  saveUninitialized: true,
+  cookie: { maxAge: 86400000 },
+  genid: () => 'test-session-id',
+};
+
+const createMockRequest = (url: string): IncomingMessage => {
+  const req = mock<IncomingMessage>();
+  req.url = url;
+  req.headers = { cookie: 'connect.sid=test-session' };
+  return req;
+};
+
+const createMockSocket = (): Duplex => {
+  const socket = mock<Duplex>();
+  socket.write = vi.fn().mockReturnValue(true);
+  socket.destroy = vi.fn();
+  return socket;
+};
+
+describe('UpgradeHandler', () => {
+  const handleUpgrade = createUpgradeHandler(sessionConfig);
+
+  it('should authorize a valid user with page access', async () => {
+    const isAccessible = await getIsAccessibleMock();
+    isAccessible.mockResolvedValue(true);
+
+    const request = createMockRequest('/yjs/507f1f77bcf86cd799439011');
+    (request as any).user = { _id: 'user1', name: 'Test User' };
+
+    const socket = createMockSocket();
+    const head = Buffer.alloc(0);
+
+    const result = await handleUpgrade(request, socket, head);
+
+    expect(result.authorized).toBe(true);
+    if (result.authorized) {
+      expect(result.pageId).toBe('507f1f77bcf86cd799439011');
+    }
+  });
+
+  it('should reject with 400 for missing/malformed URL path', async () => {
+    const request = createMockRequest('/invalid/path');
+    const socket = createMockSocket();
+    const head = Buffer.alloc(0);
+
+    const result = await handleUpgrade(request, socket, head);
+
+    expect(result.authorized).toBe(false);
+    if (!result.authorized) {
+      expect(result.statusCode).toBe(400);
+    }
+    expect(socket.write).toHaveBeenCalledWith(expect.stringContaining('400'));
+    expect(socket.destroy).toHaveBeenCalled();
+  });
+
+  it('should reject with 403 when user has no page access', async () => {
+    const isAccessible = await getIsAccessibleMock();
+    isAccessible.mockResolvedValue(false);
+
+    const request = createMockRequest('/yjs/507f1f77bcf86cd799439011');
+    (request as any).user = { _id: 'user1', name: 'Test User' };
+
+    const socket = createMockSocket();
+    const head = Buffer.alloc(0);
+
+    const result = await handleUpgrade(request, socket, head);
+
+    expect(result.authorized).toBe(false);
+    if (!result.authorized) {
+      expect(result.statusCode).toBe(403);
+    }
+    expect(socket.write).toHaveBeenCalledWith(expect.stringContaining('403'));
+    expect(socket.destroy).toHaveBeenCalled();
+  });
+
+  it('should reject with 401 when unauthenticated user has no page access', async () => {
+    const isAccessible = await getIsAccessibleMock();
+    isAccessible.mockResolvedValue(false);
+
+    const request = createMockRequest('/yjs/507f1f77bcf86cd799439011');
+    (request as any).user = undefined; // explicitly unauthenticated
+
+    const socket = createMockSocket();
+    const head = Buffer.alloc(0);
+
+    const result = await handleUpgrade(request, socket, head);
+
+    expect(result.authorized).toBe(false);
+    if (!result.authorized) {
+      expect(result.statusCode).toBe(401);
+    }
+  });
+
+  it('should allow guest user when page allows guest access', async () => {
+    const isAccessible = await getIsAccessibleMock();
+    isAccessible.mockResolvedValue(true);
+
+    const request = createMockRequest('/yjs/507f1f77bcf86cd799439011');
+    (request as any).user = undefined; // guest user
+
+    const socket = createMockSocket();
+    const head = Buffer.alloc(0);
+
+    const result = await handleUpgrade(request, socket, head);
+
+    expect(result.authorized).toBe(true);
+    if (result.authorized) {
+      expect(result.pageId).toBe('507f1f77bcf86cd799439011');
+    }
+  });
+});

+ 126 - 0
apps/app/src/server/service/yjs/upgrade-handler.ts

@@ -0,0 +1,126 @@
+import type { IPage, IUserHasId } from '@growi/core';
+import type { RequestHandler } from 'express';
+import expressSession from 'express-session';
+import type { IncomingMessage, ServerResponse } from 'http';
+import mongoose from 'mongoose';
+import passport from 'passport';
+import type { Duplex } from 'stream';
+
+import loggerFactory from '~/utils/logger';
+
+import type { PageModel } from '../../models/page';
+
+const logger = loggerFactory('growi:service:yjs:upgrade-handler');
+
+type SessionConfig = {
+  rolling: boolean;
+  secret: string;
+  resave: boolean;
+  saveUninitialized: boolean;
+  cookie: { maxAge: number };
+  genid: (req: { path: string }) => string;
+  name?: string;
+  store?: unknown;
+};
+
+type AuthenticatedRequest = IncomingMessage & {
+  user?: IUserHasId;
+};
+
+/**
+ * Run an Express-style middleware against a raw IncomingMessage
+ */
+const runMiddleware = (
+  middleware: RequestHandler,
+  req: IncomingMessage,
+): Promise<void> =>
+  new Promise((resolve, reject) => {
+    const fakeRes = {} as ServerResponse;
+    middleware(req as any, fakeRes as any, (err?: unknown) => {
+      if (err) return reject(err);
+      resolve();
+    });
+  });
+
+/**
+ * Extracts pageId from upgrade request URL.
+ * Expected format: /yjs/{pageId}
+ */
+const extractPageId = (url: string | undefined): string | null => {
+  if (url == null) return null;
+  const match = url.match(/^\/yjs\/([a-f0-9]{24})/);
+  return match?.[1] ?? null;
+};
+
+/**
+ * Rejects a WebSocket upgrade request with an HTTP error response.
+ */
+const rejectUpgrade = (
+  socket: Duplex,
+  statusCode: number,
+  message: string,
+): void => {
+  socket.write(`HTTP/1.1 ${statusCode} ${message}\r\n\r\n`);
+  socket.destroy();
+};
+
+export type UpgradeResult =
+  | { authorized: true; request: AuthenticatedRequest; pageId: string }
+  | { authorized: false; statusCode: number };
+
+/**
+ * Creates an upgrade handler that authenticates WebSocket connections
+ * using the existing express-session + passport mechanism.
+ */
+export const createUpgradeHandler = (sessionConfig: SessionConfig) => {
+  const sessionMiddleware = expressSession(sessionConfig as any);
+  const passportInit = passport.initialize();
+  const passportSession = passport.session();
+
+  return async (
+    request: IncomingMessage,
+    socket: Duplex,
+    _head: Buffer,
+  ): Promise<UpgradeResult> => {
+    const pageId = extractPageId(request.url);
+    if (pageId == null) {
+      logger.warn('Invalid URL path for Yjs upgrade', { url: request.url });
+      rejectUpgrade(socket, 400, 'Bad Request');
+      return { authorized: false, statusCode: 400 };
+    }
+
+    try {
+      // Run session + passport middleware chain
+      await runMiddleware(sessionMiddleware as RequestHandler, request);
+      await runMiddleware(passportInit as RequestHandler, request);
+      await runMiddleware(passportSession as RequestHandler, request);
+    } catch (err) {
+      logger.warn('Session/passport middleware failed on upgrade', { err });
+      rejectUpgrade(socket, 401, 'Unauthorized');
+      return { authorized: false, statusCode: 401 };
+    }
+
+    const user = (request as AuthenticatedRequest).user ?? null;
+
+    // Check page access
+    const Page = mongoose.model<IPage, PageModel>('Page');
+    const isAccessible = await Page.isAccessiblePageByViewer(pageId, user);
+
+    if (!isAccessible) {
+      const statusCode = user == null ? 401 : 403;
+      const message = user == null ? 'Unauthorized' : 'Forbidden';
+      logger.warn(`Yjs upgrade rejected: ${message}`, {
+        pageId,
+        userId: user?._id,
+      });
+      rejectUpgrade(socket, statusCode, message);
+      return { authorized: false, statusCode };
+    }
+
+    return {
+      authorized: true,
+      request: request as AuthenticatedRequest,
+      pageId,
+    };
+  };
+};

+ 205 - 0
apps/app/src/server/service/yjs/websocket-connection.integ.ts

@@ -0,0 +1,205 @@
+import http from 'node:http';
+import WebSocket, { WebSocketServer } from 'ws';
+import { docs, setPersistence, setupWSConnection } from 'y-websocket/bin/utils';
+
+/**
+ * Creates a minimal HTTP + y-websocket server for testing.
+ * No authentication — pure document sync testing.
+ */
+const createTestServer = (): { server: http.Server; wss: WebSocketServer } => {
+  const server = http.createServer();
+  const wss = new WebSocketServer({ noServer: true });
+
+  server.on('upgrade', (request, socket, head) => {
+    const url = request.url ?? '';
+    if (!url.startsWith('/yjs/')) return;
+    const pageId = url.slice('/yjs/'.length).split('?')[0];
+
+    wss.handleUpgrade(request, socket, head, (ws) => {
+      wss.emit('connection', ws, request);
+      setupWSConnection(ws, request, { docName: pageId });
+    });
+  });
+
+  return { server, wss };
+};
+
+/**
+ * Connects a WebSocket client and waits for the connection to open.
+ */
+const connectClient = (port: number, pageId: string): Promise<WebSocket> => {
+  return new Promise((resolve, reject) => {
+    const ws = new WebSocket(`ws://127.0.0.1:${port}/yjs/${pageId}`);
+    ws.binaryType = 'arraybuffer';
+    ws.on('open', () => resolve(ws));
+    ws.on('error', reject);
+  });
+};
+
+/**
+ * Waits for a WebSocket to fully close.
+ */
+const waitForClose = (ws: WebSocket): Promise<void> => {
+  return new Promise((resolve) => {
+    if (ws.readyState === WebSocket.CLOSED) return resolve();
+    ws.on('close', () => resolve());
+  });
+};
+
+describe('WebSocket Connection and Sync Flow', () => {
+  let server: http.Server;
+  let wss: WebSocketServer;
+  let port: number;
+
+  beforeAll(async () => {
+    setPersistence(null);
+
+    const testServer = createTestServer();
+    server = testServer.server;
+    wss = testServer.wss;
+
+    await new Promise<void>((resolve) => {
+      server.listen(0, '127.0.0.1', () => {
+        const addr = server.address();
+        if (addr && typeof addr === 'object') {
+          port = addr.port;
+        }
+        resolve();
+      });
+    });
+  });
+
+  afterAll(async () => {
+    for (const [name, doc] of docs) {
+      doc.destroy();
+      docs.delete(name);
+    }
+
+    await new Promise<void>((resolve) => {
+      wss.close(() => {
+        server.close(() => resolve());
+      });
+    });
+  });
+
+  afterEach(() => {
+    for (const [name, doc] of docs) {
+      doc.destroy();
+      docs.delete(name);
+    }
+  });
+
+  describe('Task 8.1: Connection and sync flow', () => {
+    it('should create a server-side Y.Doc on first client connection', async () => {
+      const pageId = 'test-page-sync-001';
+
+      const ws = await connectClient(port, pageId);
+
+      // Wait for setupWSConnection to register the doc
+      await new Promise((resolve) => setTimeout(resolve, 50));
+
+      const serverDoc = docs.get(pageId);
+      expect(serverDoc).toBeDefined();
+      expect(serverDoc!.name).toBe(pageId);
+      expect(serverDoc!.conns.size).toBe(1);
+
+      ws.close();
+    });
+
+    it('should register multiple clients on the same server-side Y.Doc', async () => {
+      const pageId = 'test-page-multi-001';
+
+      const ws1 = await connectClient(port, pageId);
+      const ws2 = await connectClient(port, pageId);
+
+      await new Promise((resolve) => setTimeout(resolve, 50));
+
+      const serverDoc = docs.get(pageId);
+      expect(serverDoc).toBeDefined();
+      expect(serverDoc!.conns.size).toBe(2);
+
+      ws1.close();
+      ws2.close();
+    });
+
+    it('should keep the server doc alive when one client disconnects', async () => {
+      const pageId = 'test-page-reconnect-001';
+
+      const ws1 = await connectClient(port, pageId);
+      const ws2 = await connectClient(port, pageId);
+
+      await new Promise((resolve) => setTimeout(resolve, 50));
+
+      // Disconnect client 1
+      ws1.close();
+      await waitForClose(ws1);
+      await new Promise((resolve) => setTimeout(resolve, 50));
+
+      // Server doc should still exist with client 2
+      const serverDoc = docs.get(pageId);
+      expect(serverDoc).toBeDefined();
+      expect(serverDoc!.conns.size).toBe(1);
+
+      ws2.close();
+    });
+  });
+
+  describe('Task 8.2: Concurrency - single Y.Doc per page', () => {
+    it('should create exactly one Y.Doc for simultaneous connections', async () => {
+      const pageId = 'test-page-concurrent-001';
+
+      // Connect multiple clients simultaneously
+      const connections = await Promise.all([
+        connectClient(port, pageId),
+        connectClient(port, pageId),
+        connectClient(port, pageId),
+      ]);
+
+      await new Promise((resolve) => setTimeout(resolve, 50));
+
+      // Verify single Y.Doc instance
+      const serverDoc = docs.get(pageId);
+      expect(serverDoc).toBeDefined();
+      expect(serverDoc!.conns.size).toBe(3);
+
+      // Only one doc for this page
+      const matchingDocs = Array.from(docs.values()).filter(
+        (d) => d.name === pageId,
+      );
+      expect(matchingDocs).toHaveLength(1);
+
+      for (const ws of connections) {
+        ws.close();
+      }
+    });
+
+    it('should handle disconnect during connect without document corruption', async () => {
+      const pageId = 'test-page-disconnect-001';
+
+      // Client 1 connects
+      const ws1 = await connectClient(port, pageId);
+      await new Promise((resolve) => setTimeout(resolve, 50));
+
+      // Write to server doc directly
+      const serverDoc = docs.get(pageId);
+      expect(serverDoc).toBeDefined();
+      serverDoc!.getText('codemirror').insert(0, 'Hello World');
+
+      // Client 2 connects and immediately disconnects
+      const ws2 = await connectClient(port, pageId);
+      ws2.close();
+      await waitForClose(ws2);
+      await new Promise((resolve) => setTimeout(resolve, 50));
+
+      // Server doc should still exist with client 1
+      const docAfter = docs.get(pageId);
+      expect(docAfter).toBeDefined();
+      expect(docAfter!.conns.size).toBe(1);
+
+      // Text should be intact
+      expect(docAfter!.getText('codemirror').toString()).toBe('Hello World');
+
+      ws1.close();
+    });
+  });
+});

+ 39 - 0
apps/app/src/server/service/yjs/y-websocket-server.d.ts

@@ -0,0 +1,39 @@
+declare module 'y-websocket/bin/utils' {
+  import type { IncomingMessage } from 'http';
+  import type { WebSocket } from 'ws';
+  import type { Awareness } from 'y-protocols/awareness';
+  import * as Y from 'yjs';
+
+  export class WSSharedDoc extends Y.Doc {
+    name: string;
+    conns: Map<WebSocket, Set<number>>;
+    awareness: Awareness;
+    whenInitialized: Promise<void>;
+    constructor(name: string);
+  }
+
+  export interface YWebsocketPersistence {
+    bindState: (docName: string, ydoc: WSSharedDoc) => void;
+    writeState: (docName: string, ydoc: WSSharedDoc) => Promise<void>;
+    provider: unknown;
+  }
+
+  export function setPersistence(
+    persistence: YWebsocketPersistence | null,
+  ): void;
+  export function getPersistence(): YWebsocketPersistence | null;
+
+  export const docs: Map<string, WSSharedDoc>;
+
+  export function getYDoc(docname: string, gc?: boolean): WSSharedDoc;
+
+  export function setupWSConnection(
+    conn: WebSocket,
+    req: IncomingMessage,
+    opts?: { docName?: string; gc?: boolean },
+  ): void;
+
+  export function setContentInitializor(
+    f: (ydoc: Y.Doc) => Promise<void>,
+  ): void;
+}

+ 20 - 6
apps/app/src/server/service/yjs/yjs.integ.ts

@@ -1,3 +1,4 @@
+import http from 'node:http';
 import { YDocStatus } from '@growi/core/dist/consts';
 import { Types } from 'mongoose';
 import type { Server } from 'socket.io';
@@ -8,11 +9,15 @@ import type { MongodbPersistence } from './extended/mongodb-persistence';
 import type { IYjsService } from './yjs';
 import { getYjsService, initializeYjsService } from './yjs';
 
-vi.mock('y-socket.io/dist/server', () => {
-  const YSocketIO = vi.fn();
-  YSocketIO.prototype.on = vi.fn();
-  YSocketIO.prototype.initialize = vi.fn();
-  return { YSocketIO };
+vi.mock('y-websocket/bin/utils', () => {
+  const docs = new Map();
+  return {
+    docs,
+    setPersistence: vi.fn(),
+    setupWSConnection: vi.fn(),
+    getYDoc: vi.fn(),
+    setContentInitializor: vi.fn(),
+  };
 });
 
 vi.mock('../revision/normalize-latest-revision-if-broken', () => ({
@@ -30,9 +35,18 @@ describe('YjsService', () => {
   describe('getYDocStatus()', () => {
     beforeAll(() => {
       const ioMock = mock<Server>();
+      const httpServer = http.createServer();
+      const sessionConfig = {
+        rolling: true,
+        secret: 'test-secret',
+        resave: false,
+        saveUninitialized: true,
+        cookie: { maxAge: 86400000 },
+        genid: () => 'test-session-id',
+      };
 
       // initialize
-      initializeYjsService(ioMock);
+      initializeYjsService(httpServer, ioMock, sessionConfig);
     });
 
     afterAll(async () => {

+ 61 - 87
apps/app/src/server/service/yjs/yjs.ts

@@ -1,49 +1,56 @@
-import type { IPage, IUserHasId } from '@growi/core';
+import type http from 'node:http';
 import { YDocStatus } from '@growi/core/dist/consts';
-import type { IncomingMessage } from 'http';
 import mongoose from 'mongoose';
 import type { Server } from 'socket.io';
-import type { Document } from 'y-socket.io/dist/server';
-import { type Document as Ydoc, YSocketIO } from 'y-socket.io/dist/server';
+import { WebSocketServer } from 'ws';
+import type { WSSharedDoc } from 'y-websocket/bin/utils';
+import { docs, setPersistence, setupWSConnection } from 'y-websocket/bin/utils';
 
-import { SocketEventName } from '~/interfaces/websocket';
 import type { SyncLatestRevisionBody } from '~/interfaces/yjs';
-import {
-  getRoomNameWithId,
-  RoomPrefix,
-} from '~/server/service/socket-io/helper';
 import loggerFactory from '~/utils/logger';
 
-import type { PageModel } from '../../models/page';
 import { Revision } from '../../models/revision';
 import { normalizeLatestRevisionIfBroken } from '../revision/normalize-latest-revision-if-broken';
 import { createIndexes } from './create-indexes';
 import { createMongoDBPersistence } from './create-mongodb-persistence';
 import { MongodbPersistence } from './extended/mongodb-persistence';
 import { syncYDoc } from './sync-ydoc';
+import { createUpgradeHandler } from './upgrade-handler';
 
 const MONGODB_PERSISTENCE_COLLECTION_NAME = 'yjs-writings';
 const MONGODB_PERSISTENCE_FLUSH_SIZE = 100;
+const YJS_PATH_PREFIX = '/yjs/';
 
 const logger = loggerFactory('growi:service:yjs');
 
-type RequestWithUser = IncomingMessage & { user: IUserHasId };
-
 export interface IYjsService {
   getYDocStatus(pageId: string): Promise<YDocStatus>;
   syncWithTheLatestRevisionForce(
     pageId: string,
     editingMarkdownLength?: number,
   ): Promise<SyncLatestRevisionBody>;
-  getCurrentYdoc(pageId: string): Ydoc | undefined;
+  getCurrentYdoc(pageId: string): WSSharedDoc | undefined;
 }
 
-class YjsService implements IYjsService {
-  private ysocketio: YSocketIO;
+type SessionConfig = {
+  rolling: boolean;
+  secret: string;
+  resave: boolean;
+  saveUninitialized: boolean;
+  cookie: { maxAge: number };
+  genid: (req: { path: string }) => string;
+  name?: string;
+  store?: unknown;
+};
 
+class YjsService implements IYjsService {
   private mdb: MongodbPersistence;
 
-  constructor(io: Server) {
+  constructor(
+    httpServer: http.Server,
+    io: Server,
+    sessionConfig: SessionConfig,
+  ) {
     const mdb = new MongodbPersistence(
       {
         // TODO: Required upgrading mongoose and unifying the versions of mongodb to omit 'as any'
@@ -57,80 +64,40 @@ class YjsService implements IYjsService {
     );
     this.mdb = mdb;
 
-    // initialize YSocketIO
-    const ysocketio = new YSocketIO(io);
-    this.injectPersistence(ysocketio, mdb);
-    ysocketio.initialize();
-    this.ysocketio = ysocketio;
-
     // create indexes
     createIndexes(MONGODB_PERSISTENCE_COLLECTION_NAME);
 
-    // register middlewares
-    this.registerAccessiblePageChecker(ysocketio);
-
-    ysocketio.on('document-loaded', async (doc: Document) => {
-      const pageId = doc.name;
-
-      const ydocStatus = await this.getYDocStatus(pageId);
-
-      syncYDoc(mdb, doc, { ydocStatus });
-    });
-
-    ysocketio.on('awareness-update', async (doc: Document) => {
-      const pageId = doc.name;
-
-      if (pageId == null) return;
-
-      const awarenessStateSize = doc.awareness.states.size;
+    // setup y-websocket persistence (includes awareness bridge and sync-on-load)
+    const persistence = createMongoDBPersistence(mdb, io, syncYDoc, (pageId) =>
+      this.getYDocStatus(pageId),
+    );
+    setPersistence(persistence);
 
-      // Triggered when awareness changes
-      io.in(getRoomNameWithId(RoomPrefix.PAGE, pageId)).emit(
-        SocketEventName.YjsAwarenessStateSizeUpdated,
-        awarenessStateSize,
-      );
+    // setup WebSocket server
+    const wss = new WebSocketServer({ noServer: true });
+    const handleUpgrade = createUpgradeHandler(sessionConfig);
 
-      // Triggered when the last user leaves the editor
-      if (awarenessStateSize === 0) {
-        const ydocStatus = await this.getYDocStatus(pageId);
-        const hasYdocsNewerThanLatestRevision =
-          ydocStatus === YDocStatus.DRAFT || ydocStatus === YDocStatus.ISOLATED;
+    httpServer.on('upgrade', async (request, socket, head) => {
+      const url = request.url ?? '';
 
-        io.in(getRoomNameWithId(RoomPrefix.PAGE, pageId)).emit(
-          SocketEventName.YjsHasYdocsNewerThanLatestRevisionUpdated,
-          hasYdocsNewerThanLatestRevision,
-        );
+      // Only handle /yjs/ paths; let Socket.IO and others pass through
+      if (!url.startsWith(YJS_PATH_PREFIX)) {
+        return;
       }
-    });
-  }
-
-  private injectPersistence(
-    ysocketio: YSocketIO,
-    mdb: MongodbPersistence,
-  ): void {
-    const persistece = createMongoDBPersistence(mdb);
 
-    // foce set to private property
-    // biome-ignore lint/complexity/useLiteralKeys: ignore
-    ysocketio['persistence'] = persistece;
-  }
+      const result = await handleUpgrade(request, socket, head);
 
-  private registerAccessiblePageChecker(ysocketio: YSocketIO): void {
-    // check accessible page
-    ysocketio.nsp?.use(async (socket, next) => {
-      // extract page id from namespace
-      const pageId = socket.nsp.name.replace(/\/yjs\|/, '');
-      const user = (socket.request as RequestWithUser).user; // should be injected by SocketIOService
-
-      const Page = mongoose.model<IPage, PageModel>('Page');
-      const isAccessible = await Page.isAccessiblePageByViewer(pageId, user);
-
-      if (!isAccessible) {
-        return next(new Error('Forbidden'));
+      if (!result.authorized) {
+        return;
       }
 
-      return next();
+      wss.handleUpgrade(result.request, socket, head, (ws) => {
+        wss.emit('connection', ws, result.request);
+        setupWSConnection(ws, result.request, { docName: result.pageId });
+      });
     });
+
+    logger.info('YjsService initialized with y-websocket');
   }
 
   public async getYDocStatus(pageId: string): Promise<YDocStatus> {
@@ -187,14 +154,14 @@ class YjsService implements IYjsService {
     pageId: string,
     editingMarkdownLength?: number,
   ): Promise<SyncLatestRevisionBody> {
-    const doc = this.ysocketio.documents.get(pageId);
+    const doc = docs.get(pageId);
 
     if (doc == null) {
       return { synced: false };
     }
 
-    const ytextLength = doc?.getText('codemirror').length;
-    syncYDoc(this.mdb, doc, true);
+    const ytextLength = doc.getText('codemirror').length;
+    await syncYDoc(this.mdb, doc, true);
 
     return {
       synced: true,
@@ -205,24 +172,31 @@ class YjsService implements IYjsService {
     };
   }
 
-  public getCurrentYdoc(pageId: string): Ydoc | undefined {
-    const currentYdoc = this.ysocketio.documents.get(pageId);
-    return currentYdoc;
+  public getCurrentYdoc(pageId: string): WSSharedDoc | undefined {
+    return docs.get(pageId);
   }
 }
 
 let _instance: YjsService;
 
-export const initializeYjsService = (io: Server): void => {
+export const initializeYjsService = (
+  httpServer: http.Server,
+  io: Server,
+  sessionConfig: SessionConfig,
+): void => {
   if (_instance != null) {
     throw new Error('YjsService is already initialized');
   }
 
+  if (httpServer == null) {
+    throw new Error("'httpServer' is required to initialize YjsService");
+  }
+
   if (io == null) {
-    throw new Error("'io' is required if initialize YjsService");
+    throw new Error("'io' is required to initialize YjsService");
   }
 
-  _instance = new YjsService(io);
+  _instance = new YjsService(httpServer, io, sessionConfig);
 };
 
 export const getYjsService = (): YjsService => {

+ 1 - 1
packages/editor/package.json

@@ -74,7 +74,7 @@
     "swr": "^2.3.2",
     "ts-deepmerge": "^6.2.0",
     "y-codemirror.next": "^0.3.5",
-    "y-socket.io": "^1.1.3",
+    "y-websocket": "^2.0.4",
     "yjs": "^13.6.19"
   }
 }

+ 8 - 6
packages/editor/src/client/stores/use-collaborative-editor-mode.ts

@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react';
 import { keymap } from '@codemirror/view';
 import type { IUserHasId } from '@growi/core/dist/interfaces';
 import { yCollab, yUndoManagerKeymap } from 'y-codemirror.next';
-import { SocketIOProvider } from 'y-socket.io';
+import { WebsocketProvider } from 'y-websocket';
 import * as Y from 'yjs';
 
 import { userColor } from '../../consts';
@@ -30,7 +30,7 @@ export const useCollaborativeEditorMode = (
       useSecondary: reviewMode,
     }) ?? {};
 
-  const [provider, setProvider] = useState<SocketIOProvider>();
+  const [provider, setProvider] = useState<WebsocketProvider>();
 
   // reset editors
   useEffect(() => {
@@ -40,7 +40,7 @@ export const useCollaborativeEditorMode = (
 
   // Setup provider
   useEffect(() => {
-    let _provider: SocketIOProvider | undefined;
+    let _provider: WebsocketProvider | undefined;
     let providerSyncHandler: (isSync: boolean) => void;
     let updateAwarenessHandler: (update: {
       added: number[];
@@ -53,8 +53,11 @@ export const useCollaborativeEditorMode = (
         return undefined;
       }
 
-      _provider = new SocketIOProvider('/', pageId, primaryDoc, {
-        autoConnect: true,
+      const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
+      const serverUrl = `${wsProtocol}//${window.location.host}/yjs`;
+
+      _provider = new WebsocketProvider(serverUrl, pageId, primaryDoc, {
+        connect: true,
         resyncInterval: 3000,
       });
 
@@ -85,7 +88,6 @@ export const useCollaborativeEditorMode = (
 
       _provider.on('sync', providerSyncHandler);
 
-      // update args type see: SocketIOProvider.Awareness.awarenessUpdate
       updateAwarenessHandler = (update: {
         added: number[];
         updated: number[];

+ 17 - 17
packages/editor/vite.config.ts

@@ -2,11 +2,10 @@ import path from 'node:path';
 import react from '@vitejs/plugin-react';
 import glob from 'glob';
 import { nodeExternals } from 'rollup-plugin-node-externals';
-import { Server } from 'socket.io';
 import type { Plugin } from 'vite';
 import { defineConfig } from 'vite';
 import dts from 'vite-plugin-dts';
-import { YSocketIO } from 'y-socket.io/dist/server';
+import { WebSocketServer } from 'ws';
 
 const excludeFiles = [
   '**/components/playground/*',
@@ -14,27 +13,28 @@ const excludeFiles = [
   '**/vite-env.d.ts',
 ];
 
-const devSocketIOPlugin = (): Plugin => ({
-  name: 'dev-socket-io',
+const devWebSocketPlugin = (): Plugin => ({
+  name: 'dev-y-websocket',
   apply: 'serve',
   configureServer(server) {
     if (!server.httpServer) return;
 
-    // setup socket.io
-    const io = new Server(server.httpServer);
-    io.on('connection', (socket) => {
-      // biome-ignore lint/suspicious/noConsole: Allow to use
-      console.log('Client connected');
+    // eslint-disable-next-line @typescript-eslint/no-require-imports
+    const { setupWSConnection } = require('y-websocket/bin/utils');
 
-      socket.on('disconnect', () => {
-        // biome-ignore lint/suspicious/noConsole: Allow to use
-        console.log('Client disconnected');
+    const wss = new WebSocketServer({ noServer: true });
+
+    server.httpServer.on('upgrade', (request, socket, head) => {
+      const url = request.url ?? '';
+      if (!url.startsWith('/yjs/')) return;
+
+      const pageId = url.slice('/yjs/'.length).split('?')[0];
+
+      wss.handleUpgrade(request, socket, head, (ws) => {
+        wss.emit('connection', ws, request);
+        setupWSConnection(ws, request, { docName: pageId });
       });
     });
-
-    // setup y-socket.io
-    const ysocketio = new YSocketIO(io);
-    ysocketio.initialize();
   },
 });
 
@@ -42,7 +42,7 @@ const devSocketIOPlugin = (): Plugin => ({
 export default defineConfig({
   plugins: [
     react(),
-    devSocketIOPlugin(),
+    devWebSocketPlugin(),
     dts({
       entryRoot: 'src',
       exclude: [...excludeFiles],

+ 120 - 83
pnpm-lock.yaml

@@ -849,9 +849,9 @@ importers:
       y-mongodb-provider:
         specifier: ^0.2.0
         version: 0.2.0(@aws-sdk/credential-providers@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))(socks@2.8.3)(yjs@13.6.19)
-      y-socket.io:
-        specifier: ^1.1.3
-        version: 1.1.3(yjs@13.6.19)
+      y-websocket:
+        specifier: ^2.0.4
+        version: 2.1.0(yjs@13.6.19)
       yjs:
         specifier: ^13.6.18
         version: 13.6.19
@@ -1431,9 +1431,9 @@ importers:
       y-codemirror.next:
         specifier: ^0.3.5
         version: 0.3.5(@codemirror/state@6.5.4)(@codemirror/view@6.39.14)(yjs@13.6.19)
-      y-socket.io:
-        specifier: ^1.1.3
-        version: 1.1.3(yjs@13.6.19)
+      y-websocket:
+        specifier: ^2.0.4
+        version: 2.1.0(yjs@13.6.19)
       yjs:
         specifier: ^13.6.19
         version: 13.6.19
@@ -6278,6 +6278,9 @@ packages:
     resolution: {integrity: sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==}
     engines: {node: '>=0.8.0'}
 
+  async-limiter@1.0.1:
+    resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
+
   async-mutex@0.4.1:
     resolution: {integrity: sha512-WfoBo4E/TbCX1G95XTjbWTE3X2XLG0m1Xbv2cwOtuPdyH9CZvnaA5nCt1ucjaKEgW2A5IF71hxrRhr83Je5xjA==}
 
@@ -14439,6 +14442,17 @@ packages:
     resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==}
     engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
 
+  ws@6.2.3:
+    resolution: {integrity: sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: ^5.0.2
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
   ws@8.18.3:
     resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==}
     engines: {node: '>=10.0.0'}
@@ -14544,8 +14558,9 @@ packages:
     peerDependencies:
       yjs: ^13.0.0
 
-  y-socket.io@1.1.3:
-    resolution: {integrity: sha512-rHalJcJjHWG3TNKJN1rcvJBDsVCdDi+3ms05JQ33+JUFzQ9pseD9SvQ7bdPnT9Ztn5t7THG/0/2MxyMdjlRHXQ==}
+  y-websocket@2.1.0:
+    resolution: {integrity: sha512-WHYDRqomaGkkaujtowCDwL8KYk+t1zQCGIgKyvxvchhjTQlMgWXRHJK+FDEcWmHA7I7o/4fy0eniOrtmz0e4mA==}
+    engines: {node: '>=16.0.0', npm: '>=8.0.0'}
     hasBin: true
     peerDependencies:
       yjs: ^13.5.6
@@ -15717,7 +15732,7 @@ snapshots:
       '@azure/core-util': 1.10.0
       '@azure/logger': 1.1.2
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6
+      https-proxy-agent: 7.0.6(supports-color@10.0.0)
       tslib: 2.8.1
     transitivePeerDependencies:
       - supports-color
@@ -15819,7 +15834,7 @@ snapshots:
       '@babel/traverse': 7.24.6
       '@babel/types': 7.25.6
       convert-source-map: 2.0.0
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -15918,7 +15933,7 @@ snapshots:
       '@babel/helper-split-export-declaration': 7.24.6
       '@babel/parser': 7.25.6
       '@babel/types': 7.25.6
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
@@ -16459,7 +16474,7 @@ snapshots:
 
   '@elastic/elasticsearch@7.17.13':
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       hpagent: 0.1.2
       ms: 2.1.3
       secure-json-parse: 2.7.0
@@ -16488,7 +16503,7 @@ snapshots:
     dependencies:
       '@opentelemetry/api': 1.9.0
       '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       hpagent: 1.2.0
       ms: 2.1.3
       secure-json-parse: 3.0.2
@@ -16501,7 +16516,7 @@ snapshots:
     dependencies:
       '@opentelemetry/api': 1.9.0
       '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0)
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       hpagent: 1.2.0
       ms: 2.1.3
       secure-json-parse: 4.0.0
@@ -16684,7 +16699,7 @@ snapshots:
   '@eslint/eslintrc@2.1.4':
     dependencies:
       ajv: 6.12.6
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       espree: 9.6.1
       globals: 13.24.0
       ignore: 5.3.2
@@ -16797,7 +16812,7 @@ snapshots:
   '@humanwhocodes/config-array@0.11.14':
     dependencies:
       '@humanwhocodes/object-schema': 2.0.3
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       minimatch: 3.1.2
     transitivePeerDependencies:
       - supports-color
@@ -16830,7 +16845,7 @@ snapshots:
       '@antfu/install-pkg': 1.1.0
       '@antfu/utils': 8.1.1
       '@iconify/types': 2.0.0
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       globals: 15.15.0
       kolorist: 1.8.0
       local-pkg: 1.1.1
@@ -17349,7 +17364,7 @@ snapshots:
     dependencies:
       agent-base: 7.1.4
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6
+      https-proxy-agent: 7.0.6(supports-color@10.0.0)
       lru-cache: 10.4.3
       socks-proxy-agent: 8.0.4
     transitivePeerDependencies:
@@ -18127,7 +18142,7 @@ snapshots:
       ajv: 8.17.1
       chalk: 4.1.2
       compare-versions: 6.1.1
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       esbuild: 0.24.0
       esutils: 2.0.3
       fs-extra: 11.2.0
@@ -18290,7 +18305,7 @@ snapshots:
 
   '@puppeteer/browsers@2.4.0':
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       extract-zip: 2.0.1
       progress: 2.0.3
       proxy-agent: 6.4.0
@@ -19309,7 +19324,7 @@ snapshots:
       '@swc-node/sourcemap-support': 0.5.1
       '@swc/core': 1.10.7(@swc/helpers@0.5.18)
       colorette: 2.0.20
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       oxc-resolver: 1.12.0
       pirates: 4.0.6
       tslib: 2.8.1
@@ -19324,7 +19339,7 @@ snapshots:
       '@swc-node/sourcemap-support': 0.5.1
       '@swc/core': 1.10.7(@swc/helpers@0.5.18)
       colorette: 2.0.20
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       oxc-resolver: 1.12.0
       pirates: 4.0.6
       tslib: 2.8.1
@@ -20515,7 +20530,7 @@ snapshots:
     dependencies:
       '@ampproject/remapping': 2.3.0
       '@bcoe/v8-coverage': 0.2.3
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       istanbul-lib-coverage: 3.2.2
       istanbul-lib-report: 3.0.1
       istanbul-lib-source-maps: 5.0.6
@@ -20724,6 +20739,7 @@ snapshots:
       level-concat-iterator: 2.0.1
       level-supports: 1.0.1
       xtend: 4.0.2
+    optional: true
 
   abstract-leveldown@6.3.0:
     dependencies:
@@ -20732,6 +20748,7 @@ snapshots:
       level-concat-iterator: 2.0.1
       level-supports: 1.0.1
       xtend: 4.0.2
+    optional: true
 
   abstract-logging@2.0.1: {}
 
@@ -20754,7 +20771,7 @@ snapshots:
 
   agent-base@6.0.2:
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -21010,6 +21027,9 @@ snapshots:
 
   async-each-series@0.1.1: {}
 
+  async-limiter@1.0.1:
+    optional: true
+
   async-mutex@0.4.1:
     dependencies:
       tslib: 2.8.1
@@ -21927,7 +21947,7 @@ snapshots:
 
   connect-mongo@4.6.0(express-session@1.18.0)(mongodb@4.17.2(@aws-sdk/client-sso-oidc@3.600.0)):
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       express-session: 1.18.0
       kruptein: 3.0.6
       mongodb: 4.17.2(@aws-sdk/client-sso-oidc@3.600.0)
@@ -22477,6 +22497,7 @@ snapshots:
     dependencies:
       abstract-leveldown: 6.2.3
       inherits: 2.0.4
+    optional: true
 
   define-data-property@1.1.4:
     dependencies:
@@ -22742,6 +22763,7 @@ snapshots:
       inherits: 2.0.4
       level-codec: 9.0.2
       level-errors: 2.0.1
+    optional: true
 
   encoding@0.1.13:
     dependencies:
@@ -22755,7 +22777,7 @@ snapshots:
   engine.io-client@6.6.4:
     dependencies:
       '@socket.io/component-emitter': 3.1.2
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       engine.io-parser: 5.2.3
       ws: 8.18.3
       xmlhttprequest-ssl: 2.1.2
@@ -22774,7 +22796,7 @@ snapshots:
       base64id: 2.0.0
       cookie: 0.7.2
       cors: 2.8.5
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       engine.io-parser: 5.2.3
       ws: 8.18.3
     transitivePeerDependencies:
@@ -22811,6 +22833,7 @@ snapshots:
   errno@0.1.8:
     dependencies:
       prr: 1.0.1
+    optional: true
 
   error-ex@1.3.2:
     dependencies:
@@ -23008,7 +23031,7 @@ snapshots:
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.6
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       doctrine: 3.0.0
       escape-string-regexp: 4.0.0
       eslint-scope: 7.2.2
@@ -23198,7 +23221,7 @@ snapshots:
 
   extract-zip@2.0.1:
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       get-stream: 5.2.0
       yauzl: 2.10.0
     optionalDependencies:
@@ -23380,7 +23403,7 @@ snapshots:
 
   follow-redirects@1.15.11(debug@4.4.3):
     optionalDependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
 
   for-each@0.3.3:
     dependencies:
@@ -23533,7 +23556,7 @@ snapshots:
   gaxios@6.7.1(encoding@0.1.13):
     dependencies:
       extend: 3.0.2
-      https-proxy-agent: 7.0.6
+      https-proxy-agent: 7.0.6(supports-color@10.0.0)
       is-stream: 2.0.0
       node-fetch: 2.7.0(encoding@0.1.13)
       uuid: 9.0.1
@@ -23611,7 +23634,7 @@ snapshots:
     dependencies:
       basic-ftp: 5.0.5
       data-uri-to-buffer: 6.0.2
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       fs-extra: 11.2.0
     transitivePeerDependencies:
       - supports-color
@@ -24127,14 +24150,14 @@ snapshots:
     dependencies:
       '@tootallnate/once': 2.0.0
       agent-base: 6.0.2
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
     transitivePeerDependencies:
       - supports-color
 
   http-proxy-agent@7.0.2:
     dependencies:
       agent-base: 7.1.4
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -24157,14 +24180,7 @@ snapshots:
   https-proxy-agent@5.0.1:
     dependencies:
       agent-base: 6.0.2
-      debug: 4.4.3(supports-color@5.5.0)
-    transitivePeerDependencies:
-      - supports-color
-
-  https-proxy-agent@7.0.6:
-    dependencies:
-      agent-base: 7.1.4
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -24241,7 +24257,8 @@ snapshots:
       pixelmatch: 5.2.1
       pngjs: 6.0.0
 
-  immediate@3.3.0: {}
+  immediate@3.3.0:
+    optional: true
 
   immer@9.0.21: {}
 
@@ -24585,7 +24602,7 @@ snapshots:
   istanbul-lib-source-maps@5.0.6:
     dependencies:
       '@jridgewell/trace-mapping': 0.3.31
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       istanbul-lib-coverage: 3.2.2
     transitivePeerDependencies:
       - supports-color
@@ -24682,7 +24699,7 @@ snapshots:
       decimal.js: 10.6.0
       html-encoding-sniffer: 4.0.0
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6
+      https-proxy-agent: 7.0.6(supports-color@10.0.0)
       is-potential-custom-element-name: 1.0.1
       nwsapi: 2.2.22
       parse5: 7.3.0
@@ -24959,18 +24976,22 @@ snapshots:
   level-codec@9.0.2:
     dependencies:
       buffer: 5.7.1
+    optional: true
 
-  level-concat-iterator@2.0.1: {}
+  level-concat-iterator@2.0.1:
+    optional: true
 
   level-errors@2.0.1:
     dependencies:
       errno: 0.1.8
+    optional: true
 
   level-iterator-stream@4.0.2:
     dependencies:
       inherits: 2.0.4
       readable-stream: 3.6.0
       xtend: 4.0.2
+    optional: true
 
   level-js@5.0.2:
     dependencies:
@@ -24978,27 +24999,32 @@ snapshots:
       buffer: 5.7.1
       inherits: 2.0.4
       ltgt: 2.2.1
+    optional: true
 
   level-packager@5.1.1:
     dependencies:
       encoding-down: 6.3.0
       levelup: 4.4.0
+    optional: true
 
   level-supports@1.0.1:
     dependencies:
       xtend: 4.0.2
+    optional: true
 
   level@6.0.1:
     dependencies:
       level-js: 5.0.2
       level-packager: 5.1.1
       leveldown: 5.6.0
+    optional: true
 
   leveldown@5.6.0:
     dependencies:
       abstract-leveldown: 6.2.3
       napi-macros: 2.0.0
       node-gyp-build: 4.1.1
+    optional: true
 
   levelup@4.4.0:
     dependencies:
@@ -25007,6 +25033,7 @@ snapshots:
       level-iterator-stream: 4.0.2
       level-supports: 1.0.1
       xtend: 4.0.2
+    optional: true
 
   leven@3.1.0: {}
 
@@ -25224,7 +25251,8 @@ snapshots:
 
   lru-cache@7.18.3: {}
 
-  ltgt@2.2.1: {}
+  ltgt@2.2.1:
+    optional: true
 
   lucene-query-parser@1.2.0: {}
 
@@ -25836,7 +25864,7 @@ snapshots:
   micromark@4.0.0:
     dependencies:
       '@types/debug': 4.1.7
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       decode-named-character-reference: 1.0.2
       devlop: 1.1.0
       micromark-core-commonmark: 2.0.1
@@ -26030,10 +26058,10 @@ snapshots:
     dependencies:
       async-mutex: 0.4.1
       camelcase: 6.3.0
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       find-cache-dir: 3.3.2
       follow-redirects: 1.15.11(debug@4.4.3)
-      https-proxy-agent: 7.0.6
+      https-proxy-agent: 7.0.6(supports-color@10.0.0)
       mongodb: 5.9.2(@aws-sdk/credential-providers@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))
       new-find-package-json: 2.0.0
       semver: 7.7.4
@@ -26137,7 +26165,7 @@ snapshots:
 
   mquery@4.0.3:
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -26210,7 +26238,8 @@ snapshots:
 
   nanoid@3.3.11: {}
 
-  napi-macros@2.0.0: {}
+  napi-macros@2.0.0:
+    optional: true
 
   natural-compare@1.4.0: {}
 
@@ -26227,7 +26256,7 @@ snapshots:
 
   new-find-package-json@2.0.0:
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -26317,7 +26346,8 @@ snapshots:
 
   node-forge@1.3.1: {}
 
-  node-gyp-build@4.1.1: {}
+  node-gyp-build@4.1.1:
+    optional: true
 
   node-gyp@10.3.1:
     dependencies:
@@ -26727,10 +26757,10 @@ snapshots:
     dependencies:
       '@tootallnate/quickjs-emscripten': 0.23.0
       agent-base: 7.1.4
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       get-uri: 6.0.3
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6
+      https-proxy-agent: 7.0.6(supports-color@10.0.0)
       pac-resolver: 7.0.1
       socks-proxy-agent: 8.0.4
     transitivePeerDependencies:
@@ -26859,7 +26889,7 @@ snapshots:
   passport-saml@3.2.4:
     dependencies:
       '@xmldom/xmldom': 0.7.13
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       passport-strategy: 1.0.0
       xml-crypto: 2.1.5
       xml-encryption: 2.0.0
@@ -27142,9 +27172,9 @@ snapshots:
   proxy-agent@6.4.0:
     dependencies:
       agent-base: 7.1.4
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       http-proxy-agent: 7.0.2
-      https-proxy-agent: 7.0.6
+      https-proxy-agent: 7.0.6(supports-color@10.0.0)
       lru-cache: 7.18.3
       pac-proxy-agent: 7.0.2
       proxy-from-env: 1.1.0
@@ -27154,7 +27184,8 @@ snapshots:
 
   proxy-from-env@1.1.0: {}
 
-  prr@1.0.1: {}
+  prr@1.0.1:
+    optional: true
 
   pseudomap@1.0.2: {}
 
@@ -27190,7 +27221,7 @@ snapshots:
 
   puppeteer-cluster@0.24.0(puppeteer@23.6.1(typescript@5.4.2)):
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       puppeteer: 23.6.1(typescript@5.4.2)
     transitivePeerDependencies:
       - supports-color
@@ -27199,7 +27230,7 @@ snapshots:
     dependencies:
       '@puppeteer/browsers': 2.4.0
       chromium-bidi: 0.8.0(devtools-protocol@0.0.1354347)
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       devtools-protocol: 0.0.1354347
       typed-query-selector: 2.12.0
       ws: 8.18.3
@@ -28018,7 +28049,7 @@ snapshots:
 
   require-in-the-middle@7.4.0:
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       module-details-from-path: 1.0.3
       resolve: 1.22.8
     transitivePeerDependencies:
@@ -28062,7 +28093,7 @@ snapshots:
 
   retry-request@4.2.2:
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       extend: 3.0.2
     transitivePeerDependencies:
       - supports-color
@@ -28548,7 +28579,7 @@ snapshots:
 
   socket.io-adapter@2.5.6:
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       ws: 8.18.3
     transitivePeerDependencies:
       - bufferutil
@@ -28558,7 +28589,7 @@ snapshots:
   socket.io-client@4.8.3:
     dependencies:
       '@socket.io/component-emitter': 3.1.2
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       engine.io-client: 6.6.4
       socket.io-parser: 4.2.5
     transitivePeerDependencies:
@@ -28569,7 +28600,7 @@ snapshots:
   socket.io-parser@4.2.5:
     dependencies:
       '@socket.io/component-emitter': 3.1.2
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
     transitivePeerDependencies:
       - supports-color
 
@@ -28578,7 +28609,7 @@ snapshots:
       accepts: 1.3.8
       base64id: 2.0.0
       cors: 2.8.5
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       engine.io: 6.6.5
       socket.io-adapter: 2.5.6
       socket.io-parser: 4.2.5
@@ -28590,7 +28621,7 @@ snapshots:
   socks-proxy-agent@7.0.0:
     dependencies:
       agent-base: 6.0.2
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       socks: 2.8.3
     transitivePeerDependencies:
       - supports-color
@@ -28598,7 +28629,7 @@ snapshots:
   socks-proxy-agent@8.0.4:
     dependencies:
       agent-base: 7.1.4
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       socks: 2.8.3
     transitivePeerDependencies:
       - supports-color
@@ -28741,7 +28772,7 @@ snapshots:
   streamroller@3.1.5:
     dependencies:
       date-format: 4.0.14
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       fs-extra: 8.1.0
     transitivePeerDependencies:
       - supports-color
@@ -28926,7 +28957,7 @@ snapshots:
       cosmiconfig: 9.0.0(typescript@5.0.4)
       css-functions-list: 3.2.2
       css-tree: 2.3.1
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       fast-glob: 3.3.2
       fastest-levenshtein: 1.0.16
       file-entry-cache: 8.0.0
@@ -28981,7 +29012,7 @@ snapshots:
     dependencies:
       component-emitter: 1.3.1
       cookiejar: 2.1.4
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       fast-safe-stringify: 2.1.1
       form-data: 4.0.4
       formidable: 3.5.4
@@ -29570,7 +29601,7 @@ snapshots:
       buffer: 6.0.3
       chalk: 4.1.2
       cli-highlight: 2.1.11
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       dotenv: 8.6.0
       glob: 7.2.3
       js-yaml: 4.1.1
@@ -29980,7 +30011,7 @@ snapshots:
   vite-node@2.1.1(@types/node@20.19.17)(sass@1.77.6)(terser@5.46.0):
     dependencies:
       cac: 6.7.14
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       pathe: 1.1.2
       vite: 5.4.21(@types/node@20.19.17)(sass@1.77.6)(terser@5.46.0)
     transitivePeerDependencies:
@@ -29999,7 +30030,7 @@ snapshots:
       '@microsoft/api-extractor': 7.43.0(@types/node@20.19.17)
       '@rollup/pluginutils': 5.2.0(rollup@4.39.0)
       '@vue/language-core': 1.8.27(typescript@5.0.4)
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       kolorist: 1.8.0
       magic-string: 0.30.11
       typescript: 5.0.4
@@ -30013,7 +30044,7 @@ snapshots:
 
   vite-tsconfig-paths@5.0.1(typescript@5.0.4)(vite@5.4.21(@types/node@20.19.17)(sass@1.77.6)(terser@5.46.0)):
     dependencies:
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       globrex: 0.1.2
       tsconfck: 3.0.3(typescript@5.0.4)
     optionalDependencies:
@@ -30049,7 +30080,7 @@ snapshots:
       '@vitest/spy': 2.1.1
       '@vitest/utils': 2.1.1
       chai: 5.1.1
-      debug: 4.4.3(supports-color@5.5.0)
+      debug: 4.4.3(supports-color@10.0.0)
       magic-string: 0.30.11
       pathe: 1.1.2
       std-env: 3.7.0
@@ -30297,6 +30328,11 @@ snapshots:
       imurmurhash: 0.1.4
       signal-exit: 4.1.0
 
+  ws@6.2.3:
+    dependencies:
+      async-limiter: 1.0.1
+    optional: true
+
   ws@8.18.3: {}
 
   x-img-diff-js@0.3.5: {}
@@ -30365,6 +30401,7 @@ snapshots:
       level: 6.0.1
       lib0: 0.2.94
       yjs: 13.6.19
+    optional: true
 
   y-mongodb-provider@0.2.0(@aws-sdk/credential-providers@3.600.0(@aws-sdk/client-sso-oidc@3.600.0))(socks@2.8.3)(yjs@13.6.19):
     dependencies:
@@ -30385,17 +30422,17 @@ snapshots:
       lib0: 0.2.94
       yjs: 13.6.19
 
-  y-socket.io@1.1.3(yjs@13.6.19):
+  y-websocket@2.1.0(yjs@13.6.19):
     dependencies:
       lib0: 0.2.94
-      socket.io: 4.8.3
-      socket.io-client: 4.8.3
-      y-leveldb: 0.1.2(yjs@13.6.19)
+      lodash.debounce: 4.0.8
       y-protocols: 1.0.6(yjs@13.6.19)
       yjs: 13.6.19
+    optionalDependencies:
+      ws: 6.2.3
+      y-leveldb: 0.1.2(yjs@13.6.19)
     transitivePeerDependencies:
       - bufferutil
-      - supports-color
       - utf-8-validate
 
   y18n@4.0.3: {}