UNPKG

open-collaboration-protocol

Version:

Open Collaboration Protocol implementation, part of the Open Collaboration Tools project

265 lines (237 loc) 12.2 kB
// ****************************************************************************** // Copyright 2024 TypeFox GmbH // This program and the accompanying materials are made available under the // terms of the MIT License, which is available in the project root. // ****************************************************************************** import type * as types from './types.js'; import { Encryption } from './messaging/encryption.js'; import { AbstractBroadcastConnection, BroadcastConnection, Handler } from './messaging/abstract-connection.js'; import { MessageTarget } from './messaging/messages.js'; import { Messages } from './messages.js'; import { MessageTransport } from './transport/transport.js'; export interface RoomHandler { onJoin(handler: Handler<[types.Peer]>): void; leave(): Promise<void>; onLeave(handler: Handler<[types.Peer]>): void; onClose(handler: Handler<[]>): void; onPermissions(handler: Handler<[types.Permissions]>): void; updatePermissions(permissions: types.Permissions): Promise<void>; } export interface PeerHandler { onJoinRequest(handler: Handler<[types.User], types.JoinResponse | undefined>): void; onInfo(handler: Handler<[types.Peer]>): void; onInit(handler: Handler<[types.InitData]>): void; init(target: MessageTarget, data: types.InitData): Promise<void>; } export interface EditorHandler { onOpen(handler: Handler<[string]>): void; open(target: MessageTarget, path: types.Path): Promise<void>; onClose(handler: Handler<[types.Path]>): void; close(path: types.Path): Promise<void>; onProposeChanges(handler: Handler<[types.Path, types.TextDiffChange[]]>): void; proposeChanges(path: types.Path, changes: types.TextDiffChange[]): Promise<void>; onCloseProposal(handler: Handler<[types.Path]>): void; closeProposal(path: types.Path): Promise<void>; } export interface FileSystemHandler { onReadFile(handler: Handler<[types.Path], types.FileData>): void; readFile(target: MessageTarget, path: types.Path): Promise<types.FileData>; onWriteFile(handler: Handler<[types.Path, types.FileData]>): void; writeFile(target: MessageTarget, path: types.Path, content: types.FileData): Promise<void>; onStat(handler: Handler<[types.Path], types.FileSystemStat>): void; stat(target: MessageTarget, path: types.Path): Promise<types.FileSystemStat>; onMkdir(handler: Handler<[types.Path]>): void; mkdir(target: MessageTarget, path: types.Path): Promise<void>; onReaddir(handler: Handler<[types.Path], types.FileSystemDirectory>): void; readdir(target: MessageTarget, path: types.Path): Promise<types.FileSystemDirectory>; onDelete(handler: Handler<[types.Path]>): void; delete(target: MessageTarget, path: types.Path): Promise<void>; onRename(handler: Handler<[types.Path, types.Path]>): void; rename(target: MessageTarget, from: types.Path, to: types.Path): Promise<void>; onChange(handler: Handler<[types.FileChangeEvent]>): void; change(event: types.FileChangeEvent): Promise<void>; } export interface SyncHandler { onDataUpdate(handler: Handler<[types.Binary]>): void; dataUpdate(data: types.Binary): Promise<void>; dataUpdate(target: MessageTarget, data: types.Binary): Promise<void>; onAwarenessUpdate(handler: Handler<[types.Binary]>): void; awarenessUpdate(data: types.Binary): Promise<void>; awarenessUpdate(target: MessageTarget, data: types.Binary): Promise<void>; onAwarenessQuery(handler: Handler<[]>): void; awarenessQuery(): Promise<void>; } export interface ChatHandler { /** * params: [message, isDirect] */ onMessage(handler: Handler<[string, boolean]>): void; sendMessage(message: string): Promise<void>; sendDirectMessage(target: MessageTarget, message: string): Promise<void>; onIsWriting(handler: Handler<[]>): void; isWriting(): Promise<void>; } export interface ProtocolBroadcastConnection extends BroadcastConnection { room: RoomHandler; peer: PeerHandler; fs: FileSystemHandler; editor: EditorHandler; sync: SyncHandler; chat: ChatHandler; } export interface ProtocolBroadcastConnectionOptions { privateKey: string; host?: types.Peer; transport: MessageTransport; } export function createConnection(options: ProtocolBroadcastConnectionOptions): ProtocolBroadcastConnection { return new ProtocolBroadcastConnectionImpl(options); } const EMTPY_HANDLER = () => { }; export class ProtocolBroadcastConnectionImpl extends AbstractBroadcastConnection { room: RoomHandler = { onJoin: handler => this.onBroadcast(Messages.Room.Joined, (origin, peer) => { this.onDidJoinRoom(peer); handler(origin, peer); }), leave: () => this.sendNotification(Messages.Room.Leave, ''), onLeave: handler => this.onBroadcast(Messages.Room.Left, (origin, peer) => { this.onDidLeaveRoom(peer); handler(origin, peer); }), onClose: handler => this.onBroadcast(Messages.Room.Closed, (origin) => { this.onDidClose(); handler(origin); }), onPermissions: handler => this.onBroadcast(Messages.Room.PermissionsUpdated, handler), updatePermissions: permissions => this.sendBroadcast(Messages.Room.PermissionsUpdated, permissions) }; peer: PeerHandler = { onJoinRequest: handler => this.onRequest(Messages.Peer.Join, handler), onInfo: handler => this.onNotification(Messages.Peer.Info, handler), onInit: handler => this.onNotification(Messages.Peer.Init, async (origin, response) => { this.onDidInit(response); handler(origin, response); }), init: (target, request) => this.sendNotification(Messages.Peer.Init, target, request) }; fs: FileSystemHandler = { onReadFile: handler => this.onRequest(Messages.FileSystem.ReadFile, handler), readFile: (target, path) => this.sendRequest(Messages.FileSystem.ReadFile, target, path), onWriteFile: handler => this.onRequest(Messages.FileSystem.WriteFile, handler), writeFile: (target, path, content) => this.sendRequest(Messages.FileSystem.WriteFile, target, path, content), onReaddir: handler => this.onRequest(Messages.FileSystem.ReadDir, handler), readdir: (target, path) => this.sendRequest(Messages.FileSystem.ReadDir, target, path), onStat: handler => this.onRequest(Messages.FileSystem.Stat, handler), stat: (target, path) => this.sendRequest(Messages.FileSystem.Stat, target, path), onMkdir: handler => this.onRequest(Messages.FileSystem.Mkdir, handler), mkdir: (target, path) => this.sendRequest(Messages.FileSystem.Mkdir, target, path), onDelete: handler => this.onRequest(Messages.FileSystem.Delete, handler), delete: (target, path) => this.sendRequest(Messages.FileSystem.Delete, target, path), onRename: handler => this.onRequest(Messages.FileSystem.Rename, handler), rename: (target, from, to) => this.sendRequest(Messages.FileSystem.Rename, target, from, to), onChange: handler => this.onBroadcast(Messages.FileSystem.Change, handler), change: event => this.sendBroadcast(Messages.FileSystem.Change, event) }; editor: EditorHandler = { onOpen: handler => this.onNotification(Messages.Editor.Open, handler), open: (target, path) => this.sendNotification(Messages.Editor.Open, target, path), onClose: handler => this.onBroadcast(Messages.Editor.Close, handler), close: path => this.sendBroadcast(Messages.Editor.Close, path), proposeChanges: (path, changes) => this.sendBroadcast(Messages.Editor.ProposeChanges, path, changes), onProposeChanges: handler => this.onBroadcast(Messages.Editor.ProposeChanges, handler), closeProposal: path => this.sendBroadcast(Messages.Editor.CloseProposal, path), onCloseProposal: handler => this.onBroadcast(Messages.Editor.CloseProposal, handler) }; sync: SyncHandler = { onDataUpdate: handler => { this.onBroadcast(Messages.Sync.DataUpdate, handler); this.onNotification(Messages.Sync.DataNotify, handler); }, dataUpdate: async (target: string | undefined | types.Binary, data?: types.Binary) => { if (typeof target === 'object') { await this.sendBroadcast(Messages.Sync.DataUpdate, target); } else { await this.sendNotification(Messages.Sync.DataNotify, target, data); } }, onAwarenessUpdate: handler => { this.onBroadcast(Messages.Sync.AwarenessUpdate, handler); this.onNotification(Messages.Sync.AwarenessNotify, handler); }, awarenessUpdate: async (target: string | undefined | types.Binary, data?: types.Binary) => { if (typeof target === 'object') { await this.sendBroadcast(Messages.Sync.AwarenessUpdate, target); } else { await this.sendNotification(Messages.Sync.AwarenessNotify, target, data); } }, onAwarenessQuery: handler => this.onBroadcast(Messages.Sync.AwarenessQuery, handler), awarenessQuery: () => this.sendBroadcast(Messages.Sync.AwarenessQuery) }; chat: ChatHandler = { sendMessage: (message) => this.sendBroadcast(Messages.Chat.ChatMessage, message), sendDirectMessage: (target, message) => this.sendNotification(Messages.Chat.DirectChatMessage, target, message), isWriting: () => this.sendBroadcast(Messages.Chat.IsWriting), onMessage: (handler) => { this.onBroadcast(Messages.Chat.ChatMessage, (orign, msg) => handler(orign, msg, false)); this.onNotification(Messages.Chat.DirectChatMessage, (orign, msg) => handler(orign, msg, true)); }, onIsWriting: (handler) => this.onBroadcast(Messages.Chat.IsWriting, handler) }; // Track peers manually for their public encryption keys private peers = new Map<string, types.Peer>(); constructor(options: ProtocolBroadcastConnectionOptions) { super({ privateKey: options.privateKey, transport: options.transport }); if (options.host) { this.onDidJoinRoom(options.host); } else { this.ready(); } // Ensure that the peer handlers are called this.room.onJoin(EMTPY_HANDLER); this.room.onLeave(EMTPY_HANDLER); this.room.onClose(EMTPY_HANDLER); this.peer.onInit(EMTPY_HANDLER); } protected override getPublicKey(origin: string): Encryption.AsymmetricKey { const peer = this.peers.get(origin); if (peer) { return { peerId: peer.id, publicKey: peer.metadata.encryption.publicKey, supportedCompression: peer.metadata.compression.supported }; } else { throw new Error('No public key found for origin ' + origin); } } private onDidJoinRoom(peer: types.Peer): void { this.peers.set(peer.id, peer); } private onDidLeaveRoom(peer: types.Peer): void { this.peers.delete(peer.id); } private onDidClose(): void { this.peers.clear(); } private onDidInit(response: types.InitData): void { for (const peer of [response.host, ...response.guests]) { this.peers.set(peer.id, peer); } this.ready(); } protected override getPublicKeys(): Encryption.AsymmetricKey[] { return Array.from(this.peers.values()).map(peer => ({ peerId: peer.id, publicKey: peer.metadata.encryption.publicKey, supportedCompression: peer.metadata.compression.supported })); } protected override getPublicKeysLength(): number { return this.peers.size; } }