open-collaboration-protocol
Version:
Open Collaboration Protocol implementation, part of the Open Collaboration Tools project
265 lines (237 loc) • 12.2 kB
text/typescript
// ******************************************************************************
// 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;
}
}