From 7fea55d81fb71a43db24e851cd3f789a96da0efa Mon Sep 17 00:00:00 2001 From: Himself65 Date: Mon, 10 Apr 2023 17:13:44 -0500 Subject: [PATCH] feat: support page sharing by meta (#1858) --- .../hooks/affine/use-affine-public-page.ts | 3 + .../src/affine/__tests__/api.spec.ts | 115 +++++++++++++++--- .../src/affine/__tests__/sync.spec.ts | 6 + packages/workspace/src/affine/api/index.ts | 4 +- 4 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 apps/web/src/hooks/affine/use-affine-public-page.ts diff --git a/apps/web/src/hooks/affine/use-affine-public-page.ts b/apps/web/src/hooks/affine/use-affine-public-page.ts new file mode 100644 index 0000000000..a9aece9fbc --- /dev/null +++ b/apps/web/src/hooks/affine/use-affine-public-page.ts @@ -0,0 +1,3 @@ +import type { AffineWorkspace } from '@affine/workspace/type'; + +export function useAffinePublicPage(workspace: AffineWorkspace) {} diff --git a/packages/workspace/src/affine/__tests__/api.spec.ts b/packages/workspace/src/affine/__tests__/api.spec.ts index ac40ade4d1..f6df0f9d84 100644 --- a/packages/workspace/src/affine/__tests__/api.spec.ts +++ b/packages/workspace/src/affine/__tests__/api.spec.ts @@ -7,15 +7,17 @@ import { readFile } from 'node:fs/promises'; import { MessageCode } from '@affine/env/constant'; import { createStatusApis } from '@affine/workspace/affine/api/status'; -import type { KeckProvider } from '@affine/workspace/affine/keck'; +import { KeckProvider } from '@affine/workspace/affine/keck'; +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 { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models'; import { assertExists } from '@blocksuite/global/utils'; -import type { Page } from '@blocksuite/store'; +import type { Page, PageMeta } from '@blocksuite/store'; import { Workspace } from '@blocksuite/store'; import { faker } from '@faker-js/faker'; import { beforeEach, describe, expect, test, vi } from 'vitest'; +import { WebSocket } from 'ws'; import { applyUpdate } from 'yjs'; import { @@ -32,6 +34,9 @@ import { setLoginStorage, } from '../login'; +// @ts-expect-error +globalThis.WebSocket = WebSocket; + let workspaceApis: ReturnType; let userApis: ReturnType; let affineAuth: ReturnType; @@ -58,6 +63,15 @@ const mockUser = { password: faker.internet.password(), }; +async function waitForConnected(provider: KeckProvider) { + return new Promise(resolve => { + provider.once('status', ({ status }: any) => { + expect(status).toBe('connected'); + resolve(); + }); + }); +} + beforeEach(() => { // create a new user for each test, so that each test can be run independently mockUser.name = faker.name.fullName(); @@ -99,30 +113,19 @@ declare global { } } -const wsUrl = `ws://127.0.0.1:3000/api/sync/`; - async function createWorkspace( workspaceApi: typeof workspaceApis, callback?: (workspace: Workspace) => void ): Promise { - const workspace = new Workspace({ - id: faker.datatype.uuid(), - }) - .register(AffineSchemas) - .register(__unstableSchemas); + const workspace = createEmptyBlockSuiteWorkspace( + faker.datatype.uuid(), + _ => undefined + ); if (callback) { callback(workspace); } const binary = Workspace.Y.encodeStateAsUpdate(workspace.doc); const data = await workspaceApi.createWorkspace(binary); - function waitForConnected(provider: KeckProvider) { - return new Promise(resolve => { - provider.once('status', ({ status }: any) => { - expect(status).toBe('connected'); - resolve(); - }); - }); - } createWorkspaceResponseSchema.parse(data); return data.id; } @@ -389,4 +392,82 @@ describe('api', () => { timeout: 30000, } ); + + test( + 'public page', + async () => { + const id = await createWorkspace(workspaceApis, workspace => { + const page = workspace.createPage('page0'); + const { frameId } = initPage(page); + page.addBlock( + 'affine:paragraph', + { + text: new page.Text('This is page0'), + }, + frameId + ); + }); + const binary = await workspaceApis.downloadWorkspace(id, false); + const workspace = createEmptyBlockSuiteWorkspace(id, () => undefined); + Workspace.Y.applyUpdate(workspace.doc, new Uint8Array(binary)); + const workspace2 = createEmptyBlockSuiteWorkspace(id, () => undefined); + { + const wsUrl = `ws://127.0.0.1:3000/api/sync/`; + const provider = new KeckProvider(wsUrl, workspace.id, workspace.doc, { + params: { token: getLoginStorage()?.token }, + // @ts-expect-error ignore the type + awareness: workspace.awarenessStore.awareness, + connect: false, + }); + const provider2 = new KeckProvider( + wsUrl, + workspace2.id, + workspace2.doc, + { + params: { token: getLoginStorage()?.token }, + // @ts-expect-error ignore the type + awareness: workspace2.awarenessStore.awareness, + connect: false, + } + ); + + provider.connect(); + provider2.connect(); + + await Promise.all([ + await waitForConnected(provider), + await waitForConnected(provider2), + ]); + const pageId = 'page0'; + const page = workspace.getPage(pageId) as Page; + expect(page).not.toBeNull(); + expect(page).not.toBeUndefined(); + workspace.setPageMeta(pageId, { + isPublic: true, + }); + + await new Promise(resolve => setTimeout(resolve, 1000)); + + const page2 = workspace2.getPage(pageId) as Page; + expect(page2).not.toBeNull(); + const meta = workspace2.meta.getPageMeta(pageId) as PageMeta; + expect(meta.isPublic).toBe(true); + + const binary = await workspaceApis.downloadPublicWorkspacePage( + id, + pageId + ); + const publicWorkspace = createEmptyBlockSuiteWorkspace( + id, + () => undefined + ); + Workspace.Y.applyUpdate(publicWorkspace.doc, new Uint8Array(binary)); + const publicPage = publicWorkspace.getPage(pageId) as Page; + expect(publicPage).not.toBeNull(); + } + }, + { + timeout: 30000, + } + ); }); diff --git a/packages/workspace/src/affine/__tests__/sync.spec.ts b/packages/workspace/src/affine/__tests__/sync.spec.ts index aa00dcf641..c502952961 100644 --- a/packages/workspace/src/affine/__tests__/sync.spec.ts +++ b/packages/workspace/src/affine/__tests__/sync.spec.ts @@ -141,7 +141,13 @@ describe('ydoc sync', () => { text: new page1.Text('hello world'), } ); + workspace1.meta.setPageMeta(pageId, { + foo: 'bar', + }); await new Promise(resolve => setTimeout(resolve, 1000)); + const pageMeta = workspace2.meta.getPageMeta(pageId); + expect(pageMeta).toBeDefined(); + expect(pageMeta?.foo).toBe('bar'); const paragraph2 = page2.getBlockById(paragraphId) as ParagraphBlockModel; const text = paragraph2.text as Text; expect(text.toString()).toEqual( diff --git a/packages/workspace/src/affine/api/index.ts b/packages/workspace/src/affine/api/index.ts index e291c2cb0e..cd30ad2766 100644 --- a/packages/workspace/src/affine/api/index.ts +++ b/packages/workspace/src/affine/api/index.ts @@ -380,7 +380,9 @@ export function createWorkspaceApis(prefixUrl = '/') { { method: 'GET', } - ).then(r => r.arrayBuffer()); + ).then(r => + r.ok ? r.arrayBuffer() : Promise.reject(new Error(`${r.status}`)) + ); }, downloadWorkspace: async ( workspaceId: string,