UNPKG

open-collaboration-protocol

Version:

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

428 lines (380 loc) 13.7 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 { isArray, isObject, isString } from '../utils/types.js'; import { VERSION } from '../utils/version.js'; /** * A collaboration message */ export interface Message { /** * Protocol version */ version: string; kind: string; metadata: MessageMetadata; } export interface MessageMetadata { encryption: MessageEncryption; compression: MessageCompression; } export namespace MessageMetadata { export function is(item: unknown): item is MessageMetadata { return isObject<MessageMetadata>(item) && MessageEncryption.is(item.encryption) && MessageCompression.is(item.compression); } } export const DEFAULT_METADATA: MessageMetadata = { encryption: { keys: [] }, compression: { algorithm: 'none' } }; export interface MessageEncryption { keys: MessageContentKey[]; } export namespace MessageEncryption { export function is(item: unknown): item is MessageEncryption { return isObject<MessageEncryption>(item) && isArray(item.keys, MessageContentKey.is); } } export interface MessageCompression { algorithm: CompressionAlgorithm; } export namespace MessageCompression { export function is(item: unknown): item is MessageCompression { return isObject<MessageCompression>(item) && isString(item.algorithm); } } export interface MessageContentKey { target: MessageTarget; key: string; iv: string; } export namespace MessageContentKey { export function is(item: unknown): item is MessageContentKey { return isObject<MessageContentKey>(item) && isString(item.target) && isString(item.key) && isString(item.iv); } } export type CompressionAlgorithm = 'none' | 'gzip' | (string & {}); export interface BinaryMessage extends Message { content: Uint8Array; } export type MessageTarget = string | undefined; export type MessageOrigin = string; export type MessageId = number | string; export namespace Message { export function is(item: unknown): item is Message { return isObject<Message>(item) && isString(item.version) && isString(item.kind) && MessageMetadata.is(item.metadata); } export function isEncrypted(item: Message): item is BinaryMessage { return (item as BinaryMessage).content instanceof Uint8Array; } } export interface AbstractErrorMessage<T> extends Message { kind: 'error'; content: T } export interface ErrorMessageContent { message: string; } export type UnknownErrorMessage = AbstractErrorMessage<unknown>; export type ErrorMessage = AbstractErrorMessage<ErrorMessageContent>; export type BinaryErrorMessage = AbstractErrorMessage<Uint8Array>; export namespace ErrorMessage { export function create(message: string): ErrorMessage { return { version: VERSION, kind: 'error', metadata: DEFAULT_METADATA, content: { message } }; } export function isAny(message: unknown): message is UnknownErrorMessage { return Message.is(message) && message.kind === 'error'; } export function is(message: unknown): message is ErrorMessage { return isAny(message) && isObject<ErrorMessageContent>(message.content) && isString(message.content.message); } export function isBinary(message: unknown): message is BinaryErrorMessage { return isAny(message) && Message.isEncrypted(message); } } /** * Request message */ export interface AbstractRequestMessage<T> extends Message { /** * The request id. */ id: number | string; kind: 'request'; /** * The origin peer id of the request. */ origin: MessageOrigin; /** * The peer id to which the request is addressed. */ target: MessageTarget; content: T; } export interface RequestMessageContent { /** * The method to be invoked. */ method: string; /** * The method's params. */ params?: unknown[]; } export type UnknownRequestMessage = AbstractRequestMessage<unknown>; export type RequestMessage = AbstractRequestMessage<RequestMessageContent>; export type EncryptedRequestMessage = AbstractRequestMessage<Uint8Array>; export namespace RequestMessage { export function create( signature: RequestType<any, any> | string, id: number | string, origin: MessageOrigin, target: MessageTarget, params?: any[] ): RequestMessage { return { version: VERSION, id, metadata: DEFAULT_METADATA, origin, target, kind: 'request', content: { method: typeof signature === 'string' ? signature : signature.method, params } }; } export function isAny(message: unknown): message is UnknownRequestMessage { return Message.is(message) && message.kind === 'request'; } export function is(message: unknown): message is RequestMessage { if (!isAny(message)) { return false; } const content = message.content; return isObject<RequestMessageContent>(content) && isString(content.method); } export function isEncrypted(message: unknown): message is EncryptedRequestMessage { return isAny(message) && Message.isEncrypted(message); } } export interface AbstractResponseMessage<T> extends Message { /** * The original request id. */ id: number | string; kind: 'response'; content: T; } export interface ResponseMessageContent { /** * The response content. */ response: unknown; } export type UnknownResponseMessage = AbstractResponseMessage<unknown>; export type ResponseMessage = AbstractResponseMessage<ResponseMessageContent>; export type EncryptedResponseMessage = AbstractResponseMessage<Uint8Array>; export namespace ResponseMessage { export function create(id: number | string, response: unknown): ResponseMessage { return { kind: 'response', version: VERSION, id, metadata: DEFAULT_METADATA, content: { response } }; } export function isAny(message: unknown): message is UnknownResponseMessage { return Message.is(message) && message.kind === 'response'; } export function is(message: unknown): message is ResponseMessage { return isAny(message) && isObject<ResponseMessageContent>(message.content) && 'response' in message.content; } export function isEncrypted(message: unknown): message is EncryptedResponseMessage { return isAny(message) && Message.isEncrypted(message); } } export interface AbstractResponseErrorMessage<T> extends Message { /** * The original request id. */ id: number | string; kind: 'response-error'; content: T; } export interface ResponseErrorMessageContent { message: string; } export type UnknownResponseErrorMessage = AbstractResponseErrorMessage<unknown>; export type ResponseErrorMessage = AbstractResponseErrorMessage<ResponseErrorMessageContent>; export type EncryptedResponseErrorMessage = AbstractResponseErrorMessage<Uint8Array>; export namespace ResponseErrorMessage { export function create(id: number | string, message: string): ResponseErrorMessage; export function create(id: number | string, message: Uint8Array): EncryptedResponseErrorMessage; export function create(id: number | string, message: string | Uint8Array): ResponseErrorMessage | EncryptedResponseErrorMessage { if (typeof message === 'string') { return { kind: 'response-error', version: VERSION, metadata: DEFAULT_METADATA, id, content: { message } }; } else { return { kind: 'response-error', version: VERSION, metadata: DEFAULT_METADATA, id, content: message }; } } export function isAny(message: unknown): message is UnknownResponseErrorMessage { return Message.is(message) && message.kind === 'response-error'; } export function is(message: unknown): message is ResponseErrorMessage { return isAny(message) && isObject<ResponseErrorMessageContent>(message.content) && isString(message.content.message); } export function isEncrypted(message: unknown): message is EncryptedResponseErrorMessage { return isAny(message) && Message.isEncrypted(message); } } export interface AbstractNotificationMessage<T> extends Message { kind: 'notification'; /** * The origin peer id of the notification. */ origin: MessageOrigin; /** * The peer id to which the notification is addressed. */ target: MessageTarget; content: T; } export interface NotificationMessageContent { /** * The method to be invoked. */ method: string; /** * The method's params. */ params?: unknown[]; } export type UnknownNotificationMessage = AbstractNotificationMessage<unknown>; export type NotificationMessage = AbstractNotificationMessage<NotificationMessageContent>; export type EncryptedNotificationMessage = AbstractNotificationMessage<Uint8Array>; export namespace NotificationMessage { export function create(signature: NotificationType<any> | string, origin: MessageOrigin, target: MessageTarget, params?: any[]): NotificationMessage { return { version: VERSION, kind: 'notification', metadata: DEFAULT_METADATA, target, origin, content: { method: typeof signature === 'string' ? signature : signature.method, params } }; } export function isAny(message: unknown): message is UnknownNotificationMessage { return Message.is(message) && message.kind === 'notification'; } export function is(message: unknown): message is NotificationMessage { if (!isAny(message)) { return false; } const content = message.content; return isObject<NotificationMessageContent>(content) && isString(content.method); } export function isEncrypted(message: unknown): message is EncryptedNotificationMessage { return isAny(message) && Message.isEncrypted(message); } } export interface AbstractBroadcastMessage<T> extends Message { kind: 'broadcast'; origin: MessageOrigin; content: T; } export interface BroadcastMessageContent { method: string; params?: unknown[]; } export type UnknownBroadcastMessage = AbstractBroadcastMessage<unknown>; export type BroadcastMessage = AbstractBroadcastMessage<BroadcastMessageContent>; export type EncryptedBroadcastMessage = AbstractBroadcastMessage<Uint8Array>; export namespace BroadcastMessage { export function create(signature: BroadcastType<any> | string, origin: string, params?: any[]): BroadcastMessage { return { version: VERSION, kind: 'broadcast', metadata: DEFAULT_METADATA, origin: origin, content: { method: typeof signature === 'string' ? signature : signature.method, params } }; } export function isAny(message: unknown): message is UnknownBroadcastMessage { return Message.is(message) && message.kind === 'broadcast'; } export function is(message: unknown): message is BroadcastMessage { if (!isAny(message)) { return false; } const content = message.content; return isObject<BroadcastMessageContent>(content) && isString(content.method); } export function isEncrypted(message: unknown): message is EncryptedBroadcastMessage { return isAny(message) && Message.isEncrypted(message); } } export interface MessageSignature { method: string } export class AbstractMessageSignature implements MessageSignature { method: string; constructor(method: string) { this.method = method; } } export class BroadcastType<P extends unknown[] = []> extends AbstractMessageSignature { public readonly _?: ['broadcast', P, void]; constructor(method: string) { super(method); } } export class RequestType<P extends unknown[], R> extends AbstractMessageSignature { public readonly _?: ['request', P, R]; constructor(method: string) { super(method); } } export class NotificationType<P extends unknown[]> extends AbstractMessageSignature { public readonly _?: ['notification', P, void]; constructor(method: string) { super(method); } }