feat: add broad cast channel provider (#1237)

This commit is contained in:
Himself65 2023-03-01 13:47:09 -06:00 committed by GitHub
parent 0df288ba2c
commit c79651ee90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 215 additions and 6 deletions

View File

@ -12,3 +12,4 @@ NODE_API_SERVER=
# save workspace to idb # save workspace to idb
ENABLE_IDB_PROVIDER=1 ENABLE_IDB_PROVIDER=1
PREFETCH_WORKSPACE=1 PREFETCH_WORKSPACE=1
ENABLE_BC_PROVIDER=1

View File

@ -32,6 +32,7 @@
"react-helmet-async": "^1.3.0", "react-helmet-async": "^1.3.0",
"swr": "^2.0.4", "swr": "^2.0.4",
"y-indexeddb": "^9.0.9", "y-indexeddb": "^9.0.9",
"y-protocols": "^1.0.5",
"yjs": "^13.5.47", "yjs": "^13.5.47",
"zod": "^3.20.6" "zod": "^3.20.6"
}, },

View File

@ -2,5 +2,8 @@ import 'dotenv/config';
export default { export default {
enableIndexedDBProvider: Boolean(process.env.ENABLE_IDB_PROVIDER ?? '1'), enableIndexedDBProvider: Boolean(process.env.ENABLE_IDB_PROVIDER ?? '1'),
enableBroadCastChannelProvider: Boolean(
process.env.ENABLE_BC_PROVIDER ?? '1'
),
prefetchWorkspace: Boolean(process.env.PREFETCH_WORKSPACE ?? '1'), prefetchWorkspace: Boolean(process.env.PREFETCH_WORKSPACE ?? '1'),
}; };

View File

@ -1,6 +1,10 @@
import { BlockSuiteWorkspace, Provider } from '../shared'; import { BlockSuiteWorkspace, Provider } from '../shared';
import { config } from '../shared/env'; import { config } from '../shared/env';
import { createIndexedDBProvider, createWebSocketProvider } from './providers'; import {
createBroadCastChannelProvider,
createIndexedDBProvider,
createWebSocketProvider,
} from './providers';
export const createAffineProviders = ( export const createAffineProviders = (
blockSuiteWorkspace: BlockSuiteWorkspace blockSuiteWorkspace: BlockSuiteWorkspace
@ -8,6 +12,8 @@ export const createAffineProviders = (
return ( return (
[ [
createWebSocketProvider(blockSuiteWorkspace), createWebSocketProvider(blockSuiteWorkspace),
config.enableBroadCastChannelProvider &&
createBroadCastChannelProvider(blockSuiteWorkspace),
config.enableIndexedDBProvider && config.enableIndexedDBProvider &&
createIndexedDBProvider(blockSuiteWorkspace), createIndexedDBProvider(blockSuiteWorkspace),
] as any[] ] as any[]
@ -19,6 +25,8 @@ export const createLocalProviders = (
): Provider[] => { ): Provider[] => {
return ( return (
[ [
config.enableBroadCastChannelProvider &&
createBroadCastChannelProvider(blockSuiteWorkspace),
config.enableIndexedDBProvider && config.enableIndexedDBProvider &&
createIndexedDBProvider(blockSuiteWorkspace), createIndexedDBProvider(blockSuiteWorkspace),
] as any[] ] as any[]

View File

@ -0,0 +1,91 @@
import { assertExists } from '@blocksuite/store';
import {
applyAwarenessUpdate,
Awareness,
encodeAwarenessUpdate,
} from 'y-protocols/awareness';
import { BlockSuiteWorkspace, BroadCastChannelProvider } from '../../../shared';
import {
BroadcastChannelMessageEvent,
getClients,
TypedBroadcastChannel,
} from './type';
export const createBroadCastChannelProvider = (
blockSuiteWorkspace: BlockSuiteWorkspace
): BroadCastChannelProvider => {
const Y = BlockSuiteWorkspace.Y;
const doc = blockSuiteWorkspace.doc;
const awareness = blockSuiteWorkspace.awarenessStore
.awareness as unknown as Awareness;
let broadcastChannel: TypedBroadcastChannel | null = null;
const handleBroadcastChannelMessage = (
event: BroadcastChannelMessageEvent
) => {
const [eventName] = event.data;
switch (eventName) {
case 'doc:diff': {
const [, diff, clientId] = event.data;
const updateV2 = Y.encodeStateAsUpdateV2(doc, diff);
broadcastChannel!.postMessage(['doc:update', updateV2, clientId]);
break;
}
case 'doc:update': {
const [, updateV2, clientId] = event.data;
Y.applyUpdateV2(doc, updateV2, clientId);
break;
}
case 'awareness:query': {
const [, clientId] = event.data;
const clients = getClients(awareness);
const update = encodeAwarenessUpdate(awareness, clients);
broadcastChannel!.postMessage(['awareness:update', update, clientId]);
break;
}
case 'awareness:update': {
const [, update, clientId] = event.data;
applyAwarenessUpdate(awareness, update, clientId);
break;
}
}
};
return {
flavour: 'broadcast-channel',
connect: () => {
assertExists(blockSuiteWorkspace.room);
broadcastChannel = Object.assign(
new BroadcastChannel(blockSuiteWorkspace.room),
{
onmessage: handleBroadcastChannelMessage,
}
);
const docDiff = Y.encodeStateVector(doc);
broadcastChannel.postMessage(['doc:diff', docDiff, awareness.clientID]);
const docUpdateV2 = Y.encodeStateAsUpdateV2(doc);
broadcastChannel.postMessage(['doc:update', docUpdateV2]);
broadcastChannel.postMessage(['awareness:query', awareness.clientID]);
const awarenessUpdate = encodeAwarenessUpdate(awareness, [
awareness.clientID,
]);
broadcastChannel.postMessage(['awareness:update', awarenessUpdate]);
const handleDocUpdate = (updateV1: Uint8Array, origin: any) => {
if (origin !== awareness.clientID) {
// not self update, ignore
return;
}
const updateV2 = Y.convertUpdateFormatV1ToV2(updateV1);
broadcastChannel?.postMessage(['doc:update', updateV2]);
};
doc.on('update', handleDocUpdate);
},
disconnect: () => {
assertExists(broadcastChannel);
broadcastChannel.close();
},
cleanup: () => {
assertExists(broadcastChannel);
broadcastChannel.close();
},
};
};

View File

@ -0,0 +1,81 @@
import { Awareness as YAwareness } from 'y-protocols/awareness';
export type ClientId = YAwareness['clientID'];
// eslint-disable-next-line @typescript-eslint/ban-types
export type DefaultClientData = {};
type EventHandler = (...args: any[]) => void;
export type DefaultEvents = {
[eventName: string]: EventHandler;
};
type EventNameWithScope<
Scope extends string,
Type extends string = string
> = `${Scope}:${Type}`;
type DataScope = 'data';
type RoomScope = 'room';
type YDocScope = 'doc';
type AwarenessScope = 'awareness';
type ObservableScope = YDocScope | AwarenessScope;
type ObservableEventName = EventNameWithScope<ObservableScope>;
type ValidEventScope = DataScope | RoomScope | ObservableScope;
type ValidateEvents<
Events extends DefaultEvents & {
[EventName in keyof Events]: EventName extends EventNameWithScope<
infer EventScope
>
? EventScope extends ValidEventScope
? Events[EventName]
: never
: Events[EventName];
}
> = Events;
export type DefaultServerToClientEvents<
ClientData extends DefaultClientData = DefaultClientData
> = ValidateEvents<{
['data:update']: (data: ClientData) => void;
['doc:diff']: (diff: ArrayBuffer) => void;
['doc:update']: (update: ArrayBuffer) => void;
['awareness:update']: (update: ArrayBuffer) => void;
}>;
export type ServerToClientEvents<
ClientData extends DefaultClientData = DefaultClientData
> = DefaultServerToClientEvents<ClientData>;
export type DefaultClientToServerEvents = ValidateEvents<{
['room:close']: () => void;
['doc:diff']: (diff: Uint8Array) => void;
['doc:update']: (update: Uint8Array, callback?: () => void) => void;
['awareness:update']: (update: Uint8Array) => void;
}>;
export type ClientToServerEvents = DefaultClientToServerEvents;
type ClientToServerEventNames = keyof ClientToServerEvents;
export type BroadcastChannelMessageData<
EventName extends ClientToServerEventNames = ClientToServerEventNames
> =
| (EventName extends ObservableEventName
? [eventName: EventName, payload: Uint8Array, clientId?: ClientId]
: never)
| [eventName: `${AwarenessScope}:query`, clientId: ClientId];
export type BroadcastChannelMessageEvent =
MessageEvent<BroadcastChannelMessageData>;
export interface TypedBroadcastChannel extends BroadcastChannel {
onmessage: ((event: BroadcastChannelMessageEvent) => void) | null;
postMessage: (message: BroadcastChannelMessageData) => void;
}
export const getClients = (awareness: YAwareness): ClientId[] => [
...awareness.getStates().keys(),
];

View File

@ -8,8 +8,9 @@ import {
LocalIndexedDBProvider, LocalIndexedDBProvider,
} from '../../shared'; } from '../../shared';
import { apis } from '../../shared/apis'; import { apis } from '../../shared/apis';
import { createBroadCastChannelProvider } from './broad-cast-channel';
export const createWebSocketProvider = ( const createWebSocketProvider = (
blockSuiteWorkspace: BlockSuiteWorkspace blockSuiteWorkspace: BlockSuiteWorkspace
): AffineWebSocketProvider => { ): AffineWebSocketProvider => {
let webSocketProvider: WebsocketProvider | null = null; let webSocketProvider: WebsocketProvider | null = null;
@ -31,6 +32,8 @@ export const createWebSocketProvider = (
params: { token: apis.auth.refresh }, params: { token: apis.auth.refresh },
// @ts-expect-error ignore the type // @ts-expect-error ignore the type
awareness: blockSuiteWorkspace.awarenessStore.awareness, awareness: blockSuiteWorkspace.awarenessStore.awareness,
// we maintain broadcast channel by ourselves
disableBc: true,
} }
); );
console.log('connect', webSocketProvider.roomname); console.log('connect', webSocketProvider.roomname);
@ -44,7 +47,7 @@ export const createWebSocketProvider = (
}; };
}; };
export const createIndexedDBProvider = ( const createIndexedDBProvider = (
blockSuiteWorkspace: BlockSuiteWorkspace blockSuiteWorkspace: BlockSuiteWorkspace
): LocalIndexedDBProvider => { ): LocalIndexedDBProvider => {
let indexdbProvider: IndexeddbPersistence | null = null; let indexdbProvider: IndexeddbPersistence | null = null;
@ -69,3 +72,9 @@ export const createIndexedDBProvider = (
}, },
}; };
}; };
export {
createBroadCastChannelProvider,
createIndexedDBProvider,
createWebSocketProvider,
};

View File

@ -91,6 +91,10 @@ export type BaseProvider = {
cleanup: () => void; cleanup: () => void;
}; };
export interface BroadCastChannelProvider extends BaseProvider {
flavour: 'broadcast-channel';
}
export interface LocalIndexedDBProvider extends BaseProvider { export interface LocalIndexedDBProvider extends BaseProvider {
flavour: 'local-indexeddb'; flavour: 'local-indexeddb';
} }
@ -99,7 +103,10 @@ export interface AffineWebSocketProvider extends BaseProvider {
flavour: 'affine-websocket'; flavour: 'affine-websocket';
} }
export type Provider = LocalIndexedDBProvider | AffineWebSocketProvider; export type Provider =
| LocalIndexedDBProvider
| AffineWebSocketProvider
| BroadCastChannelProvider;
export type AffineRemoteWorkspace = export type AffineRemoteWorkspace =
| AffineRemoteSyncedWorkspace | AffineRemoteSyncedWorkspace

View File

@ -8,6 +8,7 @@ export const publicRuntimeConfigSchema = z.object({
serverAPI: z.string(), serverAPI: z.string(),
editorVersion: z.string(), editorVersion: z.string(),
enableIndexedDBProvider: z.boolean(), enableIndexedDBProvider: z.boolean(),
enableBroadCastChannelProvider: z.boolean(),
prefetchWorkspace: z.boolean(), prefetchWorkspace: z.boolean(),
}); });

View File

@ -182,6 +182,7 @@ importers:
swr: ^2.0.4 swr: ^2.0.4
typescript: ^4.9.5 typescript: ^4.9.5
y-indexeddb: ^9.0.9 y-indexeddb: ^9.0.9
y-protocols: ^1.0.5
yjs: ^13.5.47 yjs: ^13.5.47
zod: ^3.20.6 zod: ^3.20.6
dependencies: dependencies:
@ -208,6 +209,7 @@ importers:
react-helmet-async: 1.3.0_biqbaboplfbrettd7655fr4n2y react-helmet-async: 1.3.0_biqbaboplfbrettd7655fr4n2y
swr: 2.0.4_react@18.2.0 swr: 2.0.4_react@18.2.0
y-indexeddb: 9.0.9_yjs@13.5.47 y-indexeddb: 9.0.9_yjs@13.5.47
y-protocols: 1.0.5
yjs: 13.5.47 yjs: 13.5.47
zod: 3.20.6 zod: 3.20.6
devDependencies: devDependencies:

View File

@ -1,9 +1,14 @@
export default function getConfig() { export default function getConfig() {
return { return {
publicRuntimeConfig: { publicRuntimeConfig: {
serverAPI: 'http://localhost:3000/api', PROJECT_NAME: 'AFFiNE Mock',
enableIndexedDBProvider: true, BUILD_DATE: '2021-09-01T00:00:00.000Z',
gitVersion: 'UNKNOWN',
hash: 'UNKNOWN',
editorVersion: 'UNKNOWN', editorVersion: 'UNKNOWN',
serverAPI: 'http://localhost:3000/api',
enableBroadCastChannelProvider: true,
enableIndexedDBProvider: true,
prefetchWorkspace: false, prefetchWorkspace: false,
}, },
}; };