diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e5e8c11b0c..682e5169de 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -59,6 +59,25 @@ jobs: - name: Run Type Check run: yarn typecheck + build-prototype: + name: Build Prototype + runs-on: ubuntu-latest + environment: development + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: ./.github/actions/setup-node + with: + electron-install: false + - name: Build Prototype + run: yarn nx build prototype + - name: Upload prototype artifact + uses: actions/upload-artifact@v3 + with: + name: prototype + path: ./apps/prototype/dist + if-no-files-found: error + build-server: name: Build Server runs-on: ubuntu-latest @@ -280,6 +299,49 @@ jobs: path: ./test-results if-no-files-found: ignore + e2e-prototype-test: + name: E2E Prototype Test + runs-on: ubuntu-latest + environment: development + needs: build-prototype + steps: + - uses: actions/checkout@v3 + - name: Setup Node.js + uses: ./.github/actions/setup-node + with: + playwright-install: true + electron-install: false + - name: Download prototype artifact + uses: actions/download-artifact@v3 + with: + name: prototype + path: ./apps/prototype/dist + - name: Run playwright tests + run: yarn e2e --forbid-only + working-directory: tests/affine-prototype + env: + COVERAGE: true + + # - name: Collect code coverage report + # run: yarn exec nyc report -t .nyc_output --report-dir .coverage --reporter=lcov + + # - name: Upload e2e test coverage results + # uses: codecov/codecov-action@v3 + # with: + # token: ${{ secrets.CODECOV_TOKEN }} + # files: ./.coverage/lcov.info + # flags: e2etest-prototype + # name: affine + # fail_ci_if_error: false + + - name: Upload test results + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: test-results-e2e-prototype + path: ./test-results + if-no-files-found: ignore + e2e-test: name: E2E Test runs-on: ubuntu-latest diff --git a/apps/README.md b/apps/README.md index ef34e75ebf..284c103a14 100644 --- a/apps/README.md +++ b/apps/README.md @@ -8,7 +8,7 @@ AFFiNE Developer Documentation using [waku](https://github.com/dai-shi/waku). ## electron -> `web` needs to be built before electron. +> `core` needs to be built before electron. AFFiNE Desktop (macOS, Linux and Windows Distribution) using [Electron](https://www.electronjs.org/). @@ -20,6 +20,10 @@ Server using [Nest.js](https://nestjs.com/). Storybook using [Storybook](https://storybook.js.org/). -## Core +## prototype -AFFiNE Core Application using [React.js](https://reactjs.org/). +AFFiNE Prototype using [React.js](https://reactjs.org/) + [Vite](https://vitejs.dev/). + +## core + +AFFiNE Core Application using [React.js](https://reactjs.org/) + [Webpack](https://webpack.js.org/). diff --git a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx b/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx index 43f4f55e54..7bf0a52880 100644 --- a/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx +++ b/apps/core/src/components/pure/workspace-slider-bar/WorkspaceSelector/workspace-selector.tsx @@ -4,7 +4,6 @@ import { useBlockSuiteWorkspaceName } from '@toeverything/hooks/use-block-suite- import type React from 'react'; import { useCallback } from 'react'; -import { useCurrentWorkspace } from '../../../../hooks/current/use-current-workspace'; import type { AllWorkspace } from '../../../../shared'; import { workspaceAvatarStyle } from './index.css'; import { @@ -28,9 +27,8 @@ export const WorkspaceSelector = ({ onClick, }: WorkspaceSelectorProps) => { const [name] = useBlockSuiteWorkspaceName( - currentWorkspace?.blockSuiteWorkspace + currentWorkspace.blockSuiteWorkspace ); - const [workspace] = useCurrentWorkspace(); // Open dialog when `Enter` or `Space` pressed // TODO-Doma Refactor with `@radix-ui/react-dialog` or other libraries that handle these out of the box and be accessible by default @@ -57,22 +55,20 @@ export const WorkspaceSelector = ({ data-testid="workspace-avatar" className={workspaceAvatarStyle} size={40} - workspace={currentWorkspace?.blockSuiteWorkspace ?? null} + workspace={currentWorkspace.blockSuiteWorkspace} /> {name} - {workspace && ( - - {workspace.flavour === 'local' ? ( - - ) : ( - - )} - {workspace.flavour === 'local' ? 'Local' : 'AFFiNE Cloud'} - - )} + + {currentWorkspace.flavour === 'local' ? ( + + ) : ( + + )} + {currentWorkspace.flavour === 'local' ? 'Local' : 'AFFiNE Cloud'} + ); diff --git a/apps/prototype/README.md b/apps/prototype/README.md new file mode 100644 index 0000000000..582d1347b3 --- /dev/null +++ b/apps/prototype/README.md @@ -0,0 +1,5 @@ +# AFFiNE Prototype + +> This is a prototype of the AFFiNE system to test the feasibility of the approach. +> +> It is not intended for production use. diff --git a/apps/prototype/index.html b/apps/prototype/index.html new file mode 100644 index 0000000000..233079ec9f --- /dev/null +++ b/apps/prototype/index.html @@ -0,0 +1,15 @@ + + + + + + AFFiNE Prototype + + + + + diff --git a/apps/prototype/package.json b/apps/prototype/package.json new file mode 100644 index 0000000000..588ba36cf3 --- /dev/null +++ b/apps/prototype/package.json @@ -0,0 +1,40 @@ +{ + "name": "@affine/prototype", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite --host --port 3003", + "build": "tsc -b && vite build", + "preview": "vite preview --host --port 3003" + }, + "dependencies": { + "@affine-test/fixtures": "workspace:*", + "@affine/component": "workspace:*", + "@affine/debug": "workspace:*", + "@affine/env": "workspace:*", + "@affine/graphql": "workspace:*", + "@affine/i18n": "workspace:*", + "@affine/jotai": "workspace:*", + "@affine/templates": "workspace:*", + "@affine/workspace": "workspace:*", + "@blocksuite/block-std": "0.0.0-20230809030546-32e6e21d-nightly", + "@blocksuite/blocks": "0.0.0-20230809030546-32e6e21d-nightly", + "@blocksuite/editor": "0.0.0-20230809030546-32e6e21d-nightly", + "@blocksuite/global": "0.0.0-20230809030546-32e6e21d-nightly", + "@blocksuite/icons": "^2.1.31", + "@blocksuite/lit": "0.0.0-20230809030546-32e6e21d-nightly", + "@blocksuite/store": "0.0.0-20230809030546-32e6e21d-nightly", + "@toeverything/hooks": "workspace:*", + "@toeverything/y-indexeddb": "workspace:*", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.20", + "@types/react-dom": "^18.2.7", + "@vitejs/plugin-react-swc": "^3.3.2", + "typescript": "^5.1.6", + "vite": "^4.4.9" + } +} diff --git a/apps/prototype/project.json b/apps/prototype/project.json new file mode 100644 index 0000000000..6900a83832 --- /dev/null +++ b/apps/prototype/project.json @@ -0,0 +1,16 @@ +{ + "name": "prototype", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "projectType": "application", + "sourceRoot": "apps/prototype/src", + "targets": { + "build": { + "executor": "nx:run-script", + "dependsOn": ["^build"], + "options": { + "script": "build" + }, + "outputs": ["{projectRoot}/dist"] + } + } +} diff --git a/apps/prototype/src/provider-status.tsx b/apps/prototype/src/provider-status.tsx new file mode 100644 index 0000000000..28da58ee17 --- /dev/null +++ b/apps/prototype/src/provider-status.tsx @@ -0,0 +1,57 @@ +import type { LocalIndexedDBBackgroundProvider } from '@affine/env/workspace'; +import { createIndexedDBBackgroundProvider } from '@affine/workspace/providers'; +import { assertExists } from '@blocksuite/global/utils'; +import { useDataSourceStatus } from '@toeverything/hooks/use-data-source-status'; +import React, { useCallback, useRef } from 'react'; +import ReactDOM from 'react-dom/client'; +import { Awareness } from 'y-protocols/awareness'; +import { Doc } from 'yjs'; + +const doc = new Doc(); +const map = doc.getMap(); +const awareness = new Awareness(doc); + +const indexeddbProvider = createIndexedDBBackgroundProvider('test', doc, { + awareness, +}) as LocalIndexedDBBackgroundProvider; +indexeddbProvider.connect(); + +const App = () => { + const counterRef = useRef(0); + const disposeRef = useRef(0); + const status = useDataSourceStatus(indexeddbProvider); + return ( +
+ + +
{status.type}
+
+ ); +}; + +const root = document.getElementById('root'); +assertExists(root); + +ReactDOM.createRoot(root).render( + + + +); diff --git a/apps/prototype/src/vite-env.d.ts b/apps/prototype/src/vite-env.d.ts new file mode 100644 index 0000000000..11f02fe2a0 --- /dev/null +++ b/apps/prototype/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/prototype/suite/provider-status.html b/apps/prototype/suite/provider-status.html new file mode 100644 index 0000000000..f1f5d3bb21 --- /dev/null +++ b/apps/prototype/suite/provider-status.html @@ -0,0 +1,12 @@ + + + + + + Provider status test + + +
+ + + diff --git a/apps/prototype/tsconfig.json b/apps/prototype/tsconfig.json new file mode 100644 index 0000000000..e871e17eef --- /dev/null +++ b/apps/prototype/tsconfig.json @@ -0,0 +1,40 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "moduleResolution": "bundler", + "outDir": "./lib" + }, + "include": ["./src"], + "references": [ + { + "path": "../../packages/component" + }, + { + "path": "../../packages/debug" + }, + { + "path": "../../packages/env" + }, + { + "path": "../../packages/graphql" + }, + { + "path": "../../packages/hooks" + }, + { + "path": "../../packages/i18n" + }, + { + "path": "../../packages/jotai" + }, + { + "path": "../../packages/y-indexeddb" + }, + { + "path": "../../packages/workspace" + }, + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/apps/prototype/tsconfig.node.json b/apps/prototype/tsconfig.node.json new file mode 100644 index 0000000000..02a6dd9886 --- /dev/null +++ b/apps/prototype/tsconfig.node.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "outDir": "./lib", + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/apps/prototype/vite.config.ts b/apps/prototype/vite.config.ts new file mode 100644 index 0000000000..b83f10efbe --- /dev/null +++ b/apps/prototype/vite.config.ts @@ -0,0 +1,22 @@ +import { resolve } from 'node:path'; + +import react from '@vitejs/plugin-react-swc'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + build: { + target: 'ES2022', + sourcemap: true, + rollupOptions: { + input: { + 'suite/provider-status': resolve( + __dirname, + 'suite', + 'provider-status.html' + ), + }, + }, + }, + plugins: [react()], +}); diff --git a/package.json b/package.json index 8cc8adfd75..eab02f8594 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "tests/kit", "tests/affine-legacy/*", "tests/affine-local", - "tests/affine-plugin" + "tests/affine-plugin", + "tests/affine-prototype" ], "engines": { "node": ">=18.16.1 <19.0.0" diff --git a/packages/env/src/blocksuite/index.ts b/packages/env/src/blocksuite/index.ts index 6dfd0ff613..fc82a8b98b 100644 --- a/packages/env/src/blocksuite/index.ts +++ b/packages/env/src/blocksuite/index.ts @@ -2,11 +2,7 @@ import type { Page } from '@blocksuite/store'; export async function initPageWithPreloading(page: Page) { const workspace = page.workspace; - const { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - data, - } = await import('@affine/templates/preloading.json'); + const { data } = await import('@affine/templates/preloading.json'); await page.waitForLoaded(); await workspace.importPageSnapshot(data['space:hello-world'], page.id); } diff --git a/packages/env/src/workspace.ts b/packages/env/src/workspace.ts index a1b8478941..7da044f340 100644 --- a/packages/env/src/workspace.ts +++ b/packages/env/src/workspace.ts @@ -1,3 +1,4 @@ +import type { StatusAdapter } from '@affine/y-provider'; import type { EditorContainer } from '@blocksuite/editor'; import type { Page } from '@blocksuite/store'; import type { @@ -35,7 +36,9 @@ export interface BroadCastChannelProvider extends PassiveDocProvider { /** * Long polling provider with local indexeddb */ -export interface LocalIndexedDBBackgroundProvider extends PassiveDocProvider { +export interface LocalIndexedDBBackgroundProvider + extends StatusAdapter, + PassiveDocProvider { flavour: 'local-indexeddb-background'; } @@ -43,7 +46,7 @@ export interface LocalIndexedDBDownloadProvider extends ActiveDocProvider { flavour: 'local-indexeddb'; } -export interface SQLiteProvider extends PassiveDocProvider { +export interface SQLiteProvider extends PassiveDocProvider, StatusAdapter { flavour: 'sqlite'; } diff --git a/packages/env/tsconfig.json b/packages/env/tsconfig.json index 55cd81dd9e..fc8c25e85d 100644 --- a/packages/env/tsconfig.json +++ b/packages/env/tsconfig.json @@ -4,7 +4,6 @@ "compilerOptions": { "composite": true, "noEmit": false, - "moduleResolution": "Node16", "outDir": "lib" }, "references": [ diff --git a/packages/hooks/package.json b/packages/hooks/package.json index a852acd971..9a0e793e1b 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -6,10 +6,11 @@ }, "private": true, "dependencies": { - "@affine/env": "workspace:*", - "@toeverything/y-indexeddb": "workspace:*" + "foxact": "^0.2.17" }, "devDependencies": { + "@affine/env": "workspace:*", + "@affine/y-provider": "workspace:*", "@blocksuite/block-std": "0.0.0-20230810005427-25adb757-nightly", "@blocksuite/blocks": "0.0.0-20230810005427-25adb757-nightly", "@blocksuite/editor": "0.0.0-20230810005427-25adb757-nightly", @@ -18,6 +19,7 @@ "@blocksuite/store": "0.0.0-20230810005427-25adb757-nightly" }, "peerDependencies": { + "@affine/y-provider": "workspace:*", "@blocksuite/block-std": "*", "@blocksuite/blocks": "*", "@blocksuite/editor": "*", @@ -25,5 +27,31 @@ "@blocksuite/lit": "*", "@blocksuite/store": "*" }, + "peerDependenciesMeta": { + "@affine/env": { + "optional": true + }, + "@affine/y-provider": { + "optional": true + }, + "@blocksuite/block-std": { + "optional": true + }, + "@blocksuite/blocks": { + "optional": true + }, + "@blocksuite/editor": { + "optional": true + }, + "@blocksuite/global": { + "optional": true + }, + "@blocksuite/lit": { + "optional": true + }, + "@blocksuite/store": { + "optional": true + } + }, "version": "0.8.0-canary.16" } diff --git a/packages/hooks/src/use-data-source-status.ts b/packages/hooks/src/use-data-source-status.ts new file mode 100644 index 0000000000..0d80098798 --- /dev/null +++ b/packages/hooks/src/use-data-source-status.ts @@ -0,0 +1,15 @@ +import type { Status, StatusAdapter } from '@affine/y-provider'; +import { useCallback, useSyncExternalStore } from 'react'; + +type UIStatus = + | Status + | { + type: 'unknown'; + }; + +export function useDataSourceStatus(datasource: StatusAdapter): UIStatus { + return useSyncExternalStore( + datasource.subscribeStatusChange, + useCallback(() => datasource.status, [datasource]) + ); +} diff --git a/packages/workspace/src/providers/index.ts b/packages/workspace/src/providers/index.ts index 4b39483f20..5d3fb549a6 100644 --- a/packages/workspace/src/providers/index.ts +++ b/packages/workspace/src/providers/index.ts @@ -26,10 +26,15 @@ const createIndexedDBBackgroundProvider: DocProviderCreator = ( blockSuiteWorkspace ): LocalIndexedDBBackgroundProvider => { const indexeddbProvider = create(blockSuiteWorkspace); + let connected = false; return { flavour: 'local-indexeddb-background', passive: true, + get status() { + return indexeddbProvider.status; + }, + subscribeStatusChange: indexeddbProvider.subscribeStatusChange, get connected() { return connected; }, diff --git a/packages/workspace/src/providers/sqlite-providers.ts b/packages/workspace/src/providers/sqlite-providers.ts index a05e9929ba..0363a2a06f 100644 --- a/packages/workspace/src/providers/sqlite-providers.ts +++ b/packages/workspace/src/providers/sqlite-providers.ts @@ -6,6 +6,7 @@ import { createLazyProvider, type DatasourceDocAdapter, } from '@affine/y-provider'; +import { assertExists } from '@blocksuite/global/utils'; import type { DocProviderCreator } from '@blocksuite/store'; import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store'; import type { Doc } from 'yjs'; @@ -51,6 +52,14 @@ export const createSQLiteProvider: DocProviderCreator = ( return { flavour: 'sqlite', passive: true, + get status() { + assertExists(provider); + return provider.status; + }, + subscribeStatusChange(onStatusChange) { + assertExists(provider); + return provider.subscribeStatusChange(onStatusChange); + }, connect: () => { datasource = createDatasource(id); provider = createLazyProvider(rootDoc, datasource, { origin: 'sqlite' }); diff --git a/packages/y-indexeddb/src/provider.ts b/packages/y-indexeddb/src/provider.ts index b2130655c7..5cf9840331 100644 --- a/packages/y-indexeddb/src/provider.ts +++ b/packages/y-indexeddb/src/provider.ts @@ -3,6 +3,7 @@ import { type DatasourceDocAdapter, writeOperation, } from '@affine/y-provider'; +import { assertExists } from '@blocksuite/global/utils'; import { openDB } from 'idb'; import type { Doc } from 'yjs'; import { diffUpdate, mergeUpdates } from 'yjs'; @@ -77,7 +78,6 @@ const createDatasource = ({ const merged = mergeUpdates(rows.map(({ update }) => update)); rows = [{ timestamp: Date.now(), update: merged }]; } - await writeOperation( store.put({ id: guid, @@ -112,6 +112,14 @@ export const createIndexedDBProvider = ( let provider: ReturnType | null = null; const apis = { + get status() { + assertExists(provider); + return provider.status; + }, + subscribeStatusChange(onStatusChange) { + assertExists(provider); + return provider.subscribeStatusChange(onStatusChange); + }, connect: () => { if (apis.connected) { apis.disconnect(); @@ -132,7 +140,7 @@ export const createIndexedDBProvider = ( get connected() { return provider?.connected || false; }, - }; + } satisfies IndexedDBProvider; return apis; }; diff --git a/packages/y-indexeddb/src/shared.ts b/packages/y-indexeddb/src/shared.ts index c155a220aa..3e874e05c4 100644 --- a/packages/y-indexeddb/src/shared.ts +++ b/packages/y-indexeddb/src/shared.ts @@ -1,3 +1,4 @@ +import type { StatusAdapter } from '@affine/y-provider'; import type { DBSchema, IDBPDatabase } from 'idb'; export const dbVersion = 1; @@ -8,7 +9,7 @@ export function upgradeDB(db: IDBPDatabase) { db.createObjectStore('milestone', { keyPath: 'id' }); } -export interface IndexedDBProvider { +export interface IndexedDBProvider extends StatusAdapter { connect: () => void; disconnect: () => void; cleanup: () => Promise; diff --git a/packages/y-provider/package.json b/packages/y-provider/package.json index caa5f2ee29..b2768b8ecd 100644 --- a/packages/y-provider/package.json +++ b/packages/y-provider/package.json @@ -4,6 +4,10 @@ "version": "0.8.0-canary.16", "description": "Yjs provider utilities for AFFiNE", "main": "./src/index.ts", + "module": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, "devDependencies": { "@blocksuite/store": "0.0.0-20230810005427-25adb757-nightly" }, diff --git a/packages/y-provider/src/lazy-provider.ts b/packages/y-provider/src/lazy-provider.ts index 33fb22fbfa..852cb6ff3e 100644 --- a/packages/y-provider/src/lazy-provider.ts +++ b/packages/y-provider/src/lazy-provider.ts @@ -7,7 +7,8 @@ import { encodeStateVectorFromUpdate, } from 'yjs'; -import type { DatasourceDocAdapter } from './types'; +import type { DatasourceDocAdapter, StatusAdapter } from './types'; +import type { Status } from './types'; function getDoc(doc: Doc, guid: string): Doc | undefined { if (doc.guid === guid) { @@ -33,7 +34,7 @@ export const createLazyProvider = ( rootDoc: Doc, datasource: DatasourceDocAdapter, options: LazyProviderOptions = {} -): Omit => { +): Omit & StatusAdapter => { let connected = false; const pendingMap = new Map(); // guid -> pending-updates const disposableMap = new Map void>>(); @@ -42,11 +43,59 @@ export const createLazyProvider = ( const { origin = 'lazy-provider' } = options; + // todo: should we use a real state machine here like `xstate`? + let currentStatus: Status = { + type: 'idle', + }; + let syncingStack = 0; + const callbackSet = new Set<() => void>(); + const changeStatus = (newStatus: Status) => { + // simulate a stack, each syncing and synced should be paired + if (newStatus.type === 'idle') { + if (syncingStack !== 0) { + console.error('syncingStatus !== 0, this should not happen'); + } + syncingStack = 0; + } + if (newStatus.type === 'syncing') { + syncingStack++; + } + if (newStatus.type === 'synced' || newStatus.type === 'error') { + syncingStack--; + } + + if (syncingStack < 0) { + console.error('syncingStatus < 0, this should not happen'); + } + + if (syncingStack === 0) { + currentStatus = newStatus; + } + if (newStatus.type !== 'synced') { + currentStatus = newStatus; + } + callbackSet.forEach(cb => cb()); + }; + async function syncDoc(doc: Doc) { const guid = doc.guid; - const remoteUpdate = await datasource.queryDocState(guid, { - stateVector: encodeStateVector(doc), + changeStatus({ + type: 'syncing', + }); + const remoteUpdate = await datasource + .queryDocState(guid, { + stateVector: encodeStateVector(doc), + }) + .catch(error => { + changeStatus({ + type: 'error', + error, + }); + throw error; + }); + changeStatus({ + type: 'synced', }); pendingMap.set(guid, []); @@ -59,6 +108,9 @@ export const createLazyProvider = ( ? encodeStateVectorFromUpdate(remoteUpdate) : undefined; + if (!connected) { + return; + } // perf: optimize me // it is possible the doc is only in memory but not yet in the datasource // we need to send the whole update to the datasource @@ -76,7 +128,23 @@ export const createLazyProvider = ( if (origin === updateOrigin) { return; } - datasource.sendDocUpdate(doc.guid, update).catch(console.error); + changeStatus({ + type: 'syncing', + }); + datasource + .sendDocUpdate(doc.guid, update) + .then(() => { + changeStatus({ + type: 'synced', + }); + }) + .catch(error => { + changeStatus({ + type: 'error', + error, + }); + console.error(error); + }); }; const subdocsHandler = (event: { loaded: Set; removed: Set }) => { @@ -103,6 +171,9 @@ export const createLazyProvider = ( */ function setupDatasourceListeners() { datasourceUnsub = datasource.onDocUpdate?.((guid, update) => { + changeStatus({ + type: 'syncing', + }); const doc = getDoc(rootDoc, guid); if (doc) { applyUpdate(doc, update, origin); @@ -120,6 +191,9 @@ export const createLazyProvider = ( console.warn('idb: doc not found', guid); pendingMap.set(guid, (pendingMap.get(guid) ?? []).concat(update)); } + changeStatus({ + type: 'synced', + }); }); } @@ -165,20 +239,44 @@ export const createLazyProvider = ( function connect() { connected = true; + changeStatus({ + type: 'syncing', + }); // root doc should be already loaded, // but we want to populate the cache for later update events - connectDoc(rootDoc).catch(console.error); + connectDoc(rootDoc).catch(error => { + changeStatus({ + type: 'error', + error, + }); + console.error(error); + }); + changeStatus({ + type: 'synced', + }); setupDatasourceListeners(); } async function disconnect() { connected = false; + changeStatus({ + type: 'idle', + }); disposeAll(); datasourceUnsub?.(); datasourceUnsub = undefined; } return { + get status() { + return currentStatus; + }, + subscribeStatusChange(cb: () => void) { + callbackSet.add(cb); + return () => { + callbackSet.delete(cb); + }; + }, get connected() { return connected; }, diff --git a/packages/y-provider/src/types.ts b/packages/y-provider/src/types.ts index c0af541c75..f997e230c5 100644 --- a/packages/y-provider/src/types.ts +++ b/packages/y-provider/src/types.ts @@ -1,4 +1,24 @@ -export interface DatasourceDocAdapter { +export type Status = + | { + type: 'idle'; + } + | { + type: 'syncing'; + } + | { + type: 'synced'; + } + | { + type: 'error'; + error: Error; + }; + +export interface StatusAdapter { + readonly status: Status; + subscribeStatusChange(onStatusChange: () => void): () => void; +} + +export interface DatasourceDocAdapter extends Partial { // request diff update from other clients queryDocState: ( guid: string, diff --git a/tests/affine-local/e2e/router.spec.ts b/tests/affine-local/e2e/router.spec.ts index a632abc374..b8978d14da 100644 --- a/tests/affine-local/e2e/router.spec.ts +++ b/tests/affine-local/e2e/router.spec.ts @@ -1,5 +1,5 @@ import { test } from '@affine-test/kit/playwright'; -import { openHomePage, webUrl } from '@affine-test/kit/utils/load-page'; +import { coreUrl, openHomePage } from '@affine-test/kit/utils/load-page'; import { waitEditorLoad } from '@affine-test/kit/utils/page-logic'; import { expect } from '@playwright/test'; @@ -17,7 +17,7 @@ test('goto not found workspace', async ({ page }) => { await waitEditorLoad(page); // if doesn't wait for timeout, data won't be saved into indexedDB await page.waitForTimeout(1000); - await page.goto(new URL('/workspace/invalid/all', webUrl).toString()); + await page.goto(new URL('/workspace/invalid/all', coreUrl).toString()); await page.waitForTimeout(1000); - expect(page.url()).toBe(new URL('/404', webUrl).toString()); + expect(page.url()).toBe(new URL('/404', coreUrl).toString()); }); diff --git a/tests/affine-prototype/e2e/basic.spec.ts b/tests/affine-prototype/e2e/basic.spec.ts new file mode 100644 index 0000000000..36814c7988 --- /dev/null +++ b/tests/affine-prototype/e2e/basic.spec.ts @@ -0,0 +1,12 @@ +import { test } from '@affine-test/kit/playwright'; +import { openPrototypeProviderStatusPage } from '@affine-test/kit/utils/load-page'; +import { expect } from '@playwright/test'; + +test('syncing and synced status should works', async ({ page }) => { + await openPrototypeProviderStatusPage(page); + await expect(page.getByTestId('status')).toHaveText('synced'); + await page.getByTestId('start-button').click(); + await expect(page.getByTestId('status')).toHaveText('syncing'); + await page.getByTestId('stop-button').click(); + await expect(page.getByTestId('status')).toHaveText('synced'); +}); diff --git a/tests/affine-prototype/package.json b/tests/affine-prototype/package.json new file mode 100644 index 0000000000..0a84ec9e83 --- /dev/null +++ b/tests/affine-prototype/package.json @@ -0,0 +1,13 @@ +{ + "name": "@affine-test/affine-prototype", + "private": true, + "scripts": { + "e2e": "yarn playwright test" + }, + "devDependencies": { + "@affine-test/fixtures": "workspace:*", + "@affine-test/kit": "workspace:*", + "@playwright/test": "^1.36.2" + }, + "version": "0.8.0-canary.14" +} diff --git a/tests/affine-prototype/playwright.config.ts b/tests/affine-prototype/playwright.config.ts new file mode 100644 index 0000000000..e4ea10ea9f --- /dev/null +++ b/tests/affine-prototype/playwright.config.ts @@ -0,0 +1,63 @@ +import type { + PlaywrightTestConfig, + PlaywrightWorkerOptions, +} from '@playwright/test'; +// import { devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './e2e', + fullyParallel: true, + timeout: process.env.CI ? 50_000 : 30_000, + use: { + baseURL: 'http://localhost:8080/', + browserName: + (process.env.BROWSER as PlaywrightWorkerOptions['browserName']) ?? + 'chromium', + permissions: ['clipboard-read', 'clipboard-write'], + viewport: { width: 1440, height: 800 }, + actionTimeout: 5 * 1000, + locale: 'en-US', + // Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer + // You can open traces locally(`npx playwright show-trace trace.zip`) + // or in your browser on [Playwright Trace Viewer](https://trace.playwright.dev/). + trace: 'on-first-retry', + // Record video only when retrying a test for the first time. + video: 'on-first-retry', + }, + forbidOnly: !!process.env.CI, + workers: 4, + retries: 1, + // 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot' + // default 'list' when running locally + // See https://playwright.dev/docs/test-reporters#github-actions-annotations + reporter: process.env.CI ? 'github' : 'list', + + webServer: [ + // Intentionally not building the web, reminds you to run it by yourself. + { + command: 'yarn workspace @affine/prototype preview', + port: 3003, + timeout: 120 * 1000, + reuseExistingServer: !process.env.CI, + env: { + COVERAGE: process.env.COVERAGE || 'false', + }, + }, + ], +}; + +if (process.env.CI) { + config.retries = 3; + config.workers = '50%'; +} + +export default config; diff --git a/tests/affine-prototype/tsconfig.json b/tests/affine-prototype/tsconfig.json new file mode 100644 index 0000000000..c7587f158b --- /dev/null +++ b/tests/affine-prototype/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "esModuleInterop": true, + "outDir": "lib" + }, + "include": ["e2e"], + "references": [ + { + "path": "../kit" + }, + { + "path": "../fixtures" + } + ] +} diff --git a/tests/kit/utils/load-page.ts b/tests/kit/utils/load-page.ts index f50c192c99..91c0295164 100644 --- a/tests/kit/utils/load-page.ts +++ b/tests/kit/utils/load-page.ts @@ -1,11 +1,16 @@ import type { Page } from '@playwright/test'; -export const webUrl = 'http://localhost:8080'; +export const coreUrl = 'http://localhost:8080'; +export const prototypeUrl = 'http://localhost:3003'; export async function openHomePage(page: Page) { - await page.goto(webUrl); + await page.goto(coreUrl); } export async function openPluginPage(page: Page) { - await page.goto(`${webUrl}/_plugin/index.html`); + await page.goto(`${coreUrl}/_plugin/index.html`); +} + +export async function openPrototypeProviderStatusPage(page: Page) { + await page.goto(`${prototypeUrl}/suite/provider-status.html`); } diff --git a/tsconfig.json b/tsconfig.json index 5d3e818486..d0503e10e5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -169,8 +169,14 @@ { "path": "./tests/affine-plugin" }, + { + "path": "./tests/affine-prototype" + }, { "path": "./tests/affine-legacy/0.7.0-canary.18" + }, + { + "path": "./tests/affine-legacy/0.8.0-canary.7" } ], "files": [], diff --git a/yarn.lock b/yarn.lock index aaaf1fa8d1..1aafff4763 100644 --- a/yarn.lock +++ b/yarn.lock @@ -73,6 +73,16 @@ __metadata: languageName: unknown linkType: soft +"@affine-test/affine-prototype@workspace:tests/affine-prototype": + version: 0.0.0-use.local + resolution: "@affine-test/affine-prototype@workspace:tests/affine-prototype" + dependencies: + "@affine-test/fixtures": "workspace:*" + "@affine-test/kit": "workspace:*" + "@playwright/test": ^1.36.2 + languageName: unknown + linkType: soft + "@affine-test/fixtures@workspace:*, @affine-test/fixtures@workspace:tests/fixtures": version: 0.0.0-use.local resolution: "@affine-test/fixtures@workspace:tests/fixtures" @@ -558,6 +568,38 @@ __metadata: languageName: unknown linkType: soft +"@affine/prototype@workspace:apps/prototype": + version: 0.0.0-use.local + resolution: "@affine/prototype@workspace:apps/prototype" + dependencies: + "@affine-test/fixtures": "workspace:*" + "@affine/component": "workspace:*" + "@affine/debug": "workspace:*" + "@affine/env": "workspace:*" + "@affine/graphql": "workspace:*" + "@affine/i18n": "workspace:*" + "@affine/jotai": "workspace:*" + "@affine/templates": "workspace:*" + "@affine/workspace": "workspace:*" + "@blocksuite/block-std": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/blocks": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/editor": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/global": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/icons": ^2.1.31 + "@blocksuite/lit": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/store": 0.0.0-20230809030546-32e6e21d-nightly + "@toeverything/hooks": "workspace:*" + "@toeverything/y-indexeddb": "workspace:*" + "@types/react": ^18.2.20 + "@types/react-dom": ^18.2.7 + "@vitejs/plugin-react-swc": ^3.3.2 + react: ^18.2.0 + react-dom: ^18.2.0 + typescript: ^5.1.6 + vite: ^4.4.9 + languageName: unknown + linkType: soft + "@affine/sdk@workspace:*, @affine/sdk@workspace:packages/sdk": version: 0.0.0-use.local resolution: "@affine/sdk@workspace:packages/sdk" @@ -3349,6 +3391,18 @@ __metadata: languageName: node linkType: hard +"@blocksuite/block-std@npm:0.0.0-20230809030546-32e6e21d-nightly": + version: 0.0.0-20230809030546-32e6e21d-nightly + resolution: "@blocksuite/block-std@npm:0.0.0-20230809030546-32e6e21d-nightly" + dependencies: + "@blocksuite/global": 0.0.0-20230809030546-32e6e21d-nightly + w3c-keyname: ^2.2.8 + peerDependencies: + "@blocksuite/store": 0.0.0-20230809030546-32e6e21d-nightly + checksum: 728387fe20e4b3534d6723172479b9116621a1398c9f28b4ef4e008d028717ba960e3a6d59508a45f07cfff43c71175466c10f71d48a0062938a16de2d3462c6 + languageName: node + linkType: hard + "@blocksuite/block-std@npm:0.0.0-20230810005427-25adb757-nightly": version: 0.0.0-20230810005427-25adb757-nightly resolution: "@blocksuite/block-std@npm:0.0.0-20230810005427-25adb757-nightly" @@ -3361,6 +3415,34 @@ __metadata: languageName: node linkType: hard +"@blocksuite/blocks@npm:0.0.0-20230809030546-32e6e21d-nightly": + version: 0.0.0-20230809030546-32e6e21d-nightly + resolution: "@blocksuite/blocks@npm:0.0.0-20230809030546-32e6e21d-nightly" + dependencies: + "@blocksuite/global": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/phasor": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/virgo": 0.0.0-20230809030546-32e6e21d-nightly + "@floating-ui/dom": ^1.5.1 + buffer: ^6.0.3 + date-fns: ^2.30.0 + file-type: ^16.5.4 + html2canvas: ^1.4.1 + jszip: ^3.10.1 + lit: ^2.7.6 + marked: ^4.3.0 + pdf-lib: ^1.17.1 + shiki: ^0.14.3 + turndown: ^7.1.2 + zod: ^3.21.4 + peerDependencies: + "@blocksuite/block-std": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/lit": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/store": 0.0.0-20230809030546-32e6e21d-nightly + yjs: ^13 + checksum: 8f4f4942541b6c0efd5b6527e906dfba36fb7a1b9ef32042bcfa3cbadcf0da3f55bd8f22edade27b630c62290483a1d464b300edf7406ade909aaf41e3c88b5b + languageName: node + linkType: hard + "@blocksuite/blocks@npm:0.0.0-20230810005427-25adb757-nightly": version: 0.0.0-20230810005427-25adb757-nightly resolution: "@blocksuite/blocks@npm:0.0.0-20230810005427-25adb757-nightly" @@ -3390,6 +3472,23 @@ __metadata: languageName: node linkType: hard +"@blocksuite/editor@npm:0.0.0-20230809030546-32e6e21d-nightly": + version: 0.0.0-20230809030546-32e6e21d-nightly + resolution: "@blocksuite/editor@npm:0.0.0-20230809030546-32e6e21d-nightly" + dependencies: + "@blocksuite/global": 0.0.0-20230809030546-32e6e21d-nightly + lit: ^2.7.6 + marked: ^4.3.0 + turndown: ^7.1.2 + peerDependencies: + "@blocksuite/blocks": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/lit": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/store": 0.0.0-20230809030546-32e6e21d-nightly + "@toeverything/theme": ^0.7.9 + checksum: e01ae29d424f1273a5c0ab9f969baad4b58ad8510255df4ea03addab06e12203d51edef87a36fcc20a05233d3863418540b6846229b8dd4fed1d295ce5abef78 + languageName: node + linkType: hard + "@blocksuite/editor@npm:0.0.0-20230810005427-25adb757-nightly": version: 0.0.0-20230810005427-25adb757-nightly resolution: "@blocksuite/editor@npm:0.0.0-20230810005427-25adb757-nightly" @@ -3405,6 +3504,21 @@ __metadata: languageName: node linkType: hard +"@blocksuite/global@npm:0.0.0-20230809030546-32e6e21d-nightly": + version: 0.0.0-20230809030546-32e6e21d-nightly + resolution: "@blocksuite/global@npm:0.0.0-20230809030546-32e6e21d-nightly" + dependencies: + ansi-colors: ^4.1.3 + zod: ^3.21.4 + peerDependencies: + lit: ^2.7 + peerDependenciesMeta: + lit: + optional: true + checksum: 03eb2fe544f4122f0b4369c637de0238cf8e6731ac7d47e9890a401da5ee20a21bb93c1fdd306f4379bdd5c7956b861fcad8b279145cdf370f009314a6feed72 + languageName: node + linkType: hard + "@blocksuite/global@npm:0.0.0-20230810005427-25adb757-nightly": version: 0.0.0-20230810005427-25adb757-nightly resolution: "@blocksuite/global@npm:0.0.0-20230810005427-25adb757-nightly" @@ -3440,6 +3554,19 @@ __metadata: languageName: node linkType: hard +"@blocksuite/lit@npm:0.0.0-20230809030546-32e6e21d-nightly": + version: 0.0.0-20230809030546-32e6e21d-nightly + resolution: "@blocksuite/lit@npm:0.0.0-20230809030546-32e6e21d-nightly" + dependencies: + "@blocksuite/global": 0.0.0-20230809030546-32e6e21d-nightly + lit: ^2.7.6 + peerDependencies: + "@blocksuite/block-std": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/store": 0.0.0-20230809030546-32e6e21d-nightly + checksum: c8ba7e600839fe463368d804f26fd6c369f22693681cd9c7f94500988c2a83054ef156e56e71621f1b6a67e2667703ff657d4f10e98de52f3b616ec285ee8a0c + languageName: node + linkType: hard + "@blocksuite/lit@npm:0.0.0-20230810005427-25adb757-nightly": version: 0.0.0-20230810005427-25adb757-nightly resolution: "@blocksuite/lit@npm:0.0.0-20230810005427-25adb757-nightly" @@ -3453,6 +3580,19 @@ __metadata: languageName: node linkType: hard +"@blocksuite/phasor@npm:0.0.0-20230809030546-32e6e21d-nightly": + version: 0.0.0-20230809030546-32e6e21d-nightly + resolution: "@blocksuite/phasor@npm:0.0.0-20230809030546-32e6e21d-nightly" + dependencies: + "@blocksuite/global": 0.0.0-20230809030546-32e6e21d-nightly + fractional-indexing: ^3.2.0 + peerDependencies: + nanoid: ^4 + yjs: ^13 + checksum: 46ee3d98ed054df635db30eccb716495c313ab341e4d471ae23912c855fa7c946c4b830294e93d6a7435b2c65fb1e7bdba53cf61c24cc85b13d14f50cabb9f1d + languageName: node + linkType: hard + "@blocksuite/phasor@npm:0.0.0-20230810005427-25adb757-nightly": version: 0.0.0-20230810005427-25adb757-nightly resolution: "@blocksuite/phasor@npm:0.0.0-20230810005427-25adb757-nightly" @@ -3466,6 +3606,30 @@ __metadata: languageName: node linkType: hard +"@blocksuite/store@npm:0.0.0-20230809030546-32e6e21d-nightly": + version: 0.0.0-20230809030546-32e6e21d-nightly + resolution: "@blocksuite/store@npm:0.0.0-20230809030546-32e6e21d-nightly" + dependencies: + "@blocksuite/global": 0.0.0-20230809030546-32e6e21d-nightly + "@blocksuite/virgo": 0.0.0-20230809030546-32e6e21d-nightly + "@types/flexsearch": ^0.7.3 + buffer: ^6.0.3 + flexsearch: 0.7.21 + idb-keyval: ^6.2.1 + ky: ^0.33.3 + lib0: ^0.2.78 + merge: ^2.1.1 + minimatch: ^9.0.3 + nanoid: ^4.0.2 + y-protocols: ^1.0.5 + zod: ^3.21.4 + peerDependencies: + async-call-rpc: ^6 + yjs: ^13 + checksum: 262b0858917f05eafba6c440bf7b5310e2509c1df44c67c6cc32b417af906970b1073a2864fa3c494dd993e7b6da7b16a975dbb31a240767b8e0963dd6334b3d + languageName: node + linkType: hard + "@blocksuite/store@npm:0.0.0-20230810005427-25adb757-nightly": version: 0.0.0-20230810005427-25adb757-nightly resolution: "@blocksuite/store@npm:0.0.0-20230810005427-25adb757-nightly" @@ -3490,6 +3654,19 @@ __metadata: languageName: node linkType: hard +"@blocksuite/virgo@npm:0.0.0-20230809030546-32e6e21d-nightly": + version: 0.0.0-20230809030546-32e6e21d-nightly + resolution: "@blocksuite/virgo@npm:0.0.0-20230809030546-32e6e21d-nightly" + dependencies: + "@blocksuite/global": 0.0.0-20230809030546-32e6e21d-nightly + zod: ^3.21.4 + peerDependencies: + lit: ^2.7 + yjs: ^13 + checksum: b870ef551a856e44eca743962c8b25d97e80aece9a0172c983b34ab7bb8cc4a2d3f8fa93d1550b0d2e500cf409a72aa8e72a415ccbb791598c4c8354cec1389e + languageName: node + linkType: hard + "@blocksuite/virgo@npm:0.0.0-20230810005427-25adb757-nightly": version: 0.0.0-20230810005427-25adb757-nightly resolution: "@blocksuite/virgo@npm:0.0.0-20230810005427-25adb757-nightly" @@ -11307,20 +11484,39 @@ __metadata: resolution: "@toeverything/hooks@workspace:packages/hooks" dependencies: "@affine/env": "workspace:*" + "@affine/y-provider": "workspace:*" "@blocksuite/block-std": 0.0.0-20230810005427-25adb757-nightly "@blocksuite/blocks": 0.0.0-20230810005427-25adb757-nightly "@blocksuite/editor": 0.0.0-20230810005427-25adb757-nightly "@blocksuite/global": 0.0.0-20230810005427-25adb757-nightly "@blocksuite/lit": 0.0.0-20230810005427-25adb757-nightly "@blocksuite/store": 0.0.0-20230810005427-25adb757-nightly - "@toeverything/y-indexeddb": "workspace:*" + foxact: ^0.2.17 peerDependencies: + "@affine/y-provider": "workspace:*" "@blocksuite/block-std": "*" "@blocksuite/blocks": "*" "@blocksuite/editor": "*" "@blocksuite/global": "*" "@blocksuite/lit": "*" "@blocksuite/store": "*" + peerDependenciesMeta: + "@affine/env": + optional: true + "@affine/y-provider": + optional: true + "@blocksuite/block-std": + optional: true + "@blocksuite/blocks": + optional: true + "@blocksuite/editor": + optional: true + "@blocksuite/global": + optional: true + "@blocksuite/lit": + optional: true + "@blocksuite/store": + optional: true languageName: unknown linkType: soft @@ -12141,6 +12337,17 @@ __metadata: languageName: node linkType: hard +"@types/react@npm:^18.2.20": + version: 18.2.20 + resolution: "@types/react@npm:18.2.20" + dependencies: + "@types/prop-types": "*" + "@types/scheduler": "*" + csstype: ^3.0.2 + checksum: 30f699c60e5e4bfef273ce64d320651cdd60f5c6a08361c6c7eca8cebcccda1ac953d2ee57c9f321b5ae87f8a62c72b6d35ca42df0e261d337849952daab2141 + languageName: node + linkType: hard + "@types/responselike@npm:^1.0.0": version: 1.0.0 resolution: "@types/responselike@npm:1.0.0" @@ -27533,7 +27740,7 @@ __metadata: languageName: node linkType: hard -"react-dom@npm:18.2.0": +"react-dom@npm:18.2.0, react-dom@npm:^18.2.0": version: 18.2.0 resolution: "react-dom@npm:18.2.0" dependencies: