UNPKG

open-collaboration-monaco

Version:

Connect a single Monaco Editor to an Open Collaboration Tools session

236 lines (205 loc) 7.98 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 { 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(); }