diff --git a/apps/web/src/blocksuite/providers/index.ts b/apps/web/src/blocksuite/providers/index.ts index 251976348f..2d4f0df6f5 100644 --- a/apps/web/src/blocksuite/providers/index.ts +++ b/apps/web/src/blocksuite/providers/index.ts @@ -32,7 +32,7 @@ const createAffineWebSocketProvider = ( blockSuiteWorkspace.id, blockSuiteWorkspace.doc, { - params: { token: getLoginStorage()?.token ?? '' }, + params: { token: getLoginStorage()?.refresh ?? '' }, // @ts-expect-error ignore the type awareness: blockSuiteWorkspace.awarenessStore.awareness, // we maintain broadcast channel by ourselves diff --git a/packages/workspace/package.json b/packages/workspace/package.json index 5d1ddf6e5c..91383be347 100644 --- a/packages/workspace/package.json +++ b/packages/workspace/package.json @@ -23,5 +23,9 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "zod": "^3.21.4" + }, + "devDependencies": { + "@types/ws": "^8.5.4", + "ws": "^8.13.0" } } diff --git a/packages/workspace/src/affine/__tests__/sync.spec.ts b/packages/workspace/src/affine/__tests__/sync.spec.ts new file mode 100644 index 0000000000..2a3505d9d4 --- /dev/null +++ b/packages/workspace/src/affine/__tests__/sync.spec.ts @@ -0,0 +1,166 @@ +import type { Workspace } from '@affine/workspace/affine/api'; +import { + createWorkspaceApis, + PermissionType, +} from '@affine/workspace/affine/api'; +import type { LoginResponse } from '@affine/workspace/affine/login'; +import { loginResponseSchema } from '@affine/workspace/affine/login'; +import { WebsocketProvider } from '@affine/workspace/affine/sync'; +import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils'; +import user1 from '@affine-test/fixtures/built-in-user1.json'; +import user2 from '@affine-test/fixtures/built-in-user2.json'; +import type { ParagraphBlockModel } from '@blocksuite/blocks/models'; +import type { Page, Text } from '@blocksuite/store'; +import { uuidv4, Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; +import { beforeEach, describe, expect, test, vi } from 'vitest'; +import WebSocket from 'ws'; + +const currentTokenRef = { + current: null as LoginResponse | null, +}; + +vi.stubGlobal('localStorage', { + getItem: () => JSON.stringify(currentTokenRef.current), + setItem: () => null, +}); + +let workspaceApis: ReturnType; + +let user1Token: LoginResponse; +let user2Token: LoginResponse; + +beforeEach(() => { + workspaceApis = createWorkspaceApis('http://127.0.0.1:3000/'); +}); + +beforeEach(async () => { + const data = await fetch('http://127.0.0.1:3000/api/user/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: 'DebugLoginUser', + email: user1.email, + password: user1.password, + }), + }).then(r => r.json()); + loginResponseSchema.parse(data); + user1Token = data; + const data2 = await fetch('http://127.0.0.1:3000/api/user/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + type: 'DebugLoginUser', + email: user2.email, + password: user2.password, + }), + }).then(r => r.json()); + loginResponseSchema.parse(data2); + user2Token = data2; +}); + +const wsUrl = `ws://127.0.0.1:3000/api/sync/`; + +describe('ydoc sync', () => { + test( + 'page', + async () => { + currentTokenRef.current = user1Token; + const list = await workspaceApis.getWorkspaces(); + const root = list.find( + workspace => workspace.permission === PermissionType.Owner + ) as Workspace; + expect(root).toBeDefined(); + const binary = await workspaceApis.downloadWorkspace(root.id); + const workspace1 = createEmptyBlockSuiteWorkspace( + root.id, + (k: string) => ({ api: '/api/workspace', token: user1Token.token }[k]) + ); + const workspace2 = createEmptyBlockSuiteWorkspace( + root.id, + (k: string) => ({ api: '/api/workspace', token: user2Token.token }[k]) + ); + BlockSuiteWorkspace.Y.applyUpdate(workspace1.doc, new Uint8Array(binary)); + BlockSuiteWorkspace.Y.applyUpdate(workspace2.doc, new Uint8Array(binary)); + const provider1 = new WebsocketProvider( + wsUrl, + workspace1.id, + workspace1.doc, + { + // @ts-expect-error ignore the error + WebSocketPolyfill: WebSocket, + params: { token: user1Token.refresh }, + // @ts-expect-error ignore the type + awareness: workspace1.awarenessStore.awareness, + disableBc: true, + connect: false, + } + ); + + const provider2 = new WebsocketProvider( + wsUrl, + workspace2.id, + workspace2.doc, + { + // @ts-expect-error ignore the error + WebSocketPolyfill: WebSocket, + params: { token: user2Token.refresh }, + // @ts-expect-error ignore the type + awareness: workspace2.awarenessStore.awareness, + disableBc: true, + connect: false, + } + ); + + provider1.connect(); + provider2.connect(); + + function waitForConnected(provider: WebsocketProvider) { + return new Promise(resolve => { + provider.once('status', ({ status }: any) => { + expect(status).toBe('connected'); + resolve(); + }); + }); + } + + await Promise.all([ + waitForConnected(provider1), + waitForConnected(provider2), + ]); + + const pageId = uuidv4(); + const page1 = workspace1.createPage(pageId); + const pageBlockId = page1.addBlock('affine:page', { + title: new page1.Text(''), + }); + page1.addBlock('affine:surface', {}, null); + const frameId = page1.addBlock('affine:frame', {}, pageBlockId); + const paragraphId = page1.addBlock('affine:paragraph', {}, frameId); + await new Promise(resolve => setTimeout(resolve, 1000)); + expect(workspace2.getPage(pageId)).toBeDefined(); + expect(workspace2.doc.getMap(`space:${pageId}`).toJSON()).toEqual( + workspace1.doc.getMap(`space:${pageId}`).toJSON() + ); + const page2 = workspace2.getPage(pageId) as Page; + page1.updateBlockById(paragraphId, { + text: new page1.Text('hello world'), + }); + await new Promise(resolve => setTimeout(resolve, 1000)); + const paragraph2 = page2.getBlockById(paragraphId) as ParagraphBlockModel; + const text = paragraph2.text as Text; + expect(text.toString()).toEqual( + page1.getBlockById(paragraphId)?.text?.toString() + ); + + provider1.disconnect(); + provider2.disconnect(); + }, + { + timeout: 30000, + } + ); +}); diff --git a/yarn.lock b/yarn.lock index 7d4519f447..5fc6b58f2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -234,6 +234,7 @@ __metadata: "@affine/env": "workspace:*" "@blocksuite/blocks": 0.5.0-20230324040005-14417c2 "@blocksuite/store": 0.5.0-20230324040005-14417c2 + "@types/ws": ^8.5.4 firebase: ^9.18.0 jotai: ^2.0.3 js-base64: ^3.7.5 @@ -241,6 +242,7 @@ __metadata: lib0: ^0.2.73 react: ^18.2.0 react-dom: ^18.2.0 + ws: ^8.13.0 zod: ^3.21.4 languageName: unknown linkType: soft @@ -6742,6 +6744,15 @@ __metadata: languageName: node linkType: hard +"@types/ws@npm:^8.5.4": + version: 8.5.4 + resolution: "@types/ws@npm:8.5.4" + dependencies: + "@types/node": "*" + checksum: fefbad20d211929bb996285c4e6f699b12192548afedbe4930ab4384f8a94577c9cd421acaad163cacd36b88649509970a05a0b8f20615b30c501ed5269038d1 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.0 resolution: "@types/yargs-parser@npm:21.0.0" @@ -19256,7 +19267,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.2.3": +"ws@npm:^8.13.0, ws@npm:^8.2.3": version: 8.13.0 resolution: "ws@npm:8.13.0" peerDependencies: