open-collaboration-monaco
Version:
Connect a single Monaco Editor to an Open Collaboration Tools session
236 lines (205 loc) • 7.98 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 { ConnectionProvider, SocketIoTransportProvider } from 'open-collaboration-protocol';
import { CollaborationInstance, UsersChangeEvent, FileNameChangeEvent } from './collaboration-instance.js';
import * as types from 'open-collaboration-protocol';
import { createRoom, joinRoom, login } from './collaboration-connection.js';
import * as monaco from 'monaco-editor';
let connectionProvider: ConnectionProvider | undefined;
let instance: CollaborationInstance | undefined;
types.initializeProtocol({
cryptoModule: globalThis.crypto
});
export type MonacoCollabCallbacks = {
onUserRequestsAccess: (user: types.User) => Promise<boolean>;
/**
* reports the status when joining or creating a room
* @param info information about the changed status
*/
statusReporter?: (info: types.Info) => void;
}
export type MonacoCollabOptions = {
serverUrl: string;
callbacks: MonacoCollabCallbacks;
userToken?: string;
roomToken?: string;
useCookieAuth?: boolean;
loginPageOpener?: (token: string, authenticationMetadata: types.AuthMetadata) => Promise<boolean>;
};
export type OtherUserData = {peer: types.Peer, color: string};
export type UserData = {me: types.Peer, others: OtherUserData[]};
export type MonacoCollabApi = {
createRoom: () => Promise<string | undefined>
joinRoom: (roomToken: string) => Promise<string | undefined>
leaveRoom: () => void
login: () => Promise<string | undefined>
logout: () => Promise<void | undefined>
isLoggedIn: () => Promise<boolean>
setEditor: (editor: monaco.editor.IStandaloneCodeEditor) => void
getUserData: () => Promise<UserData | undefined>
onUsersChanged: (evt: UsersChangeEvent) => void
onFileNameChange: (callback: FileNameChangeEvent) => void
getCurrentConnection: () => types.ProtocolBroadcastConnection | undefined
followUser: (id?: string) => void
getFollowedUser: () => string | undefined
setFileName: (fileName: string) => void
getFileName: () => string | undefined
setWorkspaceName: (workspaceName: string) => void
getWorkspaceName: () => string | undefined
}
export function monacoCollab(options: MonacoCollabOptions): MonacoCollabApi {
connectionProvider = new ConnectionProvider({
url: options.serverUrl,
authenticationHandler: options.loginPageOpener ?? (async (_token, metaData) => {
// If this returns null, it means the window could not be opened and the authentication failed
return window.open(metaData.loginPageUrl, '_blank') !== null;
}),
transports: [SocketIoTransportProvider],
userToken: options.userToken,
useCookieAuth: options.useCookieAuth,
fetch: async (url, options) => {
const response = await fetch(url, options);
return {
ok: response.ok,
status: response.status,
json: async () => response.json(),
text: async () => response.text()
};
}
});
const doCreateRoom = async () => {
console.log('Creating room');
if (!connectionProvider) {
console.log('No OCT Server configured.');
throw new Error('No OCT Server configured.');
}
instance = await createRoom(connectionProvider, options.callbacks);
if (instance) {
return instance.roomId;
}
throw new Error('Failed to create room');
};
const doJoinRoom = async (roomToken: string) => {
console.log('Joining room', roomToken);
if (!connectionProvider) {
console.log('No OCT Server configured.');
throw new Error('No OCT Server configured.');
}
const res = await joinRoom(connectionProvider, options.callbacks, roomToken);
if (res && 'message' in res) {
console.log('Failed to join room:', res.message);
throw new Error('Failed to join room:' + res.message);
} else {
instance = res;
return instance.roomId;
}
};
const doLogin = async () => {
if (!connectionProvider) {
console.log('No OCT Server configured.');
throw new Error('No OCT Server configured.');
}
await login(connectionProvider);
return connectionProvider.authToken;
};
const doSetEditor = (editor: monaco.editor.IStandaloneCodeEditor) => {
if (instance) {
instance.setEditor(editor);
}
};
const doGetUserData = async () => {
let data: UserData | undefined;
if (instance) {
const me: types.Peer = await instance.ownUserData;
const others = instance.connectedUsers.map(
user => ({
peer: user.peer,
color: user.color ?? 'rgba(0, 0, 0, 0.5)'
}));
data = {me, others};
}
return data;
};
const registerUserChangeHandler = (evt: UsersChangeEvent) => {
if (instance) {
instance.onUsersChanged(evt);
}
};
const doFollowUser = (id?: string) => {
if (instance) {
instance.followUser(id);
}
};
const doGetFollowedUser = () => {
if (instance) {
return instance.following;
}
return undefined;
};
const doSetFileName = (fileName: string) => {
if (instance) {
instance.setFileName(fileName);
}
};
const doGetWorkspaceName = () => {
if (instance) {
return instance.workspaceName;
}
return undefined;
};
const doGetFileName = () => {
if (instance) {
return instance.fileName;
}
return undefined;
};
const registerFileNameChangeHandler = (callback: FileNameChangeEvent) => {
if (instance) {
instance.onFileNameChange(callback);
}
};
const doSetWorkspaceName = (workspaceName: string) => {
if (instance) {
instance.workspaceName = workspaceName;
}
};
const isLoggedIn = async () => {
if (!connectionProvider) {
return false;
}
if (options.useCookieAuth) {
const valid = await fetch(options.serverUrl + '/api/login/validate', {
credentials: 'include',
method: 'POST',
});
return valid.ok && (await valid.json())?.valid;
} else {
return !!connectionProvider.authToken;
}
};
return {
createRoom: doCreateRoom,
joinRoom: doJoinRoom,
leaveRoom: () => instance?.leaveRoom(),
login: doLogin,
logout: async () => connectionProvider?.logout(),
isLoggedIn: isLoggedIn,
setEditor: doSetEditor,
getUserData: doGetUserData,
onUsersChanged: registerUserChangeHandler,
onFileNameChange: registerFileNameChangeHandler,
followUser: doFollowUser,
getFollowedUser: doGetFollowedUser,
getCurrentConnection: () => instance?.getCurrentConnection(),
setFileName: doSetFileName,
getFileName: doGetFileName,
getWorkspaceName: doGetWorkspaceName,
setWorkspaceName: doSetWorkspaceName
};
}
export function deactivate() {
instance?.dispose();
}