diff --git a/src/api/base.ts b/src/api/base.ts index 5f091fcc..de24020f 100644 --- a/src/api/base.ts +++ b/src/api/base.ts @@ -239,10 +239,12 @@ export class BaseAPI { // Reference: "github:overleaf/overleaf/services/web/frontend/js/ide/connection/ConnectionManager.js#L137" _initSocketV0(identity:Identity, query?:string) { - const url = new URL(this.url).origin + (query ?? ''); - return (require('socket.io-client').connect as any)(url, { + const baseUrl = new URL(this.url).origin; + const queryString = query?.startsWith('?') ? query.slice(1) : query; + return (require('socket.io-client').connect as any)(baseUrl, { reconnect: false, 'force new connection': true, + ...(queryString ? {query: queryString} : {}), extraHeaders: { 'Origin': new URL(this.url).origin, 'Cookie': identity.cookies, @@ -601,7 +603,7 @@ export class BaseAPI { }; this.setIdentity(identity); - return this.request('POST', `project/${projectId}/compile?auto_compile=true`, body, (res) => { + return this.request('POST', `project/${projectId}/compile`, body, (res) => { const compile = JSON.parse(res!) as CompileResponseSchema; return {compile}; }, {'X-Csrf-Token': identity.csrfToken}); diff --git a/src/api/socketio.ts b/src/api/socketio.ts index ccd2a611..bd66735b 100644 --- a/src/api/socketio.ts +++ b/src/api/socketio.ts @@ -75,7 +75,7 @@ export interface EventsHandler { type ConnectionScheme = 'Alt' | 'v1' | 'v2'; export class SocketIOAPI { - private scheme: ConnectionScheme = 'v1'; + private scheme: ConnectionScheme = 'v2'; private record?: Promise; private _handlers: Array = []; @@ -289,31 +289,65 @@ export class SocketIOAPI { * @returns {Promise} */ async joinProject(project_id:string): Promise { + if (this.scheme==='v2') { + try { + return await this.joinProjectByV2(); + } catch (err) { + // Old Overleaf stacks may not support auto join via handshake. + // Fallback to legacy joinProject event to keep compatibility. + this.scheme = 'v1'; + this.init(); + return this.joinProjectByV1(project_id); + } + } + + try { + return await this.joinProjectByV1(project_id); + } catch (err) { + if (!this.shouldFallbackToV2(err)) { + throw err; + } + // New Overleaf stacks require projectId in handshake query. + this.scheme = 'v2'; + this.init(); + return this.joinProjectByV2(); + } + } + + private async joinProjectByV1(project_id:string): Promise { const timeoutPromise: Promise = new Promise((_, reject) => { setTimeout(() => { reject('timeout'); }, 5000); }); - switch(this.scheme) { - case 'Alt': - case 'v1': - const joinPromise = this.emit('joinProject', {project_id}) - .then((returns:[ProjectEntity, string, number]) => { - const [project, permissionsLevel, protocolVersion] = returns; - this.record = Promise.resolve(project); - return project; - }); - const rejectPromise = new Promise((_, reject) => { - this.socket.on('connectionRejected', (err:any) => { - this.scheme = 'v2'; - reject(err.message); - }); - }); - return Promise.race([joinPromise, rejectPromise, timeoutPromise]); - case 'v2': - return Promise.race([this.record!, timeoutPromise]); - } + const joinPromise = this.emit('joinProject', {project_id}) + .then((returns:[ProjectEntity, string, number]) => { + const [project, permissionsLevel, protocolVersion] = returns; + this.record = Promise.resolve(project); + return project; + }); + const rejectPromise: Promise = new Promise((_, reject) => { + this.socket.on('connectionRejected', (err:any) => { + reject(err?.message ?? err); + }); + }); + + return Promise.race([joinPromise, rejectPromise, timeoutPromise]); + } + + private async joinProjectByV2(): Promise { + const timeoutPromise: Promise = new Promise((_, reject) => { + setTimeout(() => { + reject('timeout'); + }, 5000); + }); + return Promise.race([this.record!, timeoutPromise]); + } + + private shouldFallbackToV2(err: unknown): boolean { + const message = String(err ?? '').toLowerCase(); + return message.includes('missing/bad ?projectid') || message.includes('bad projectid') || message.includes('missing projectid'); } /** diff --git a/src/compile/compileManager.ts b/src/compile/compileManager.ts index afe2d04e..ee5bd292 100644 --- a/src/compile/compileManager.ts +++ b/src/compile/compileManager.ts @@ -226,9 +226,12 @@ export class CompileManager { .then(status => status ? vscode.commands.executeCommand(`${ROOT_NAME}.compileManager.compileErrorCheck`, uri) - : Promise.reject() + : undefined ) .then(async (hasError) => { + if (hasError===undefined) { + return; + } if (hasError) { await this.update('failed'); } else { @@ -239,6 +242,10 @@ export class CompileManager { pdfViewRecord[identifier] && Object.values(pdfViewRecord[identifier]).forEach( (record) => record.doc.refresh() ); + }) + .catch(async (error) => { + console.error(`${ELEGANT_NAME}: compile chain failed`, error); + await this.update('failed'); }); } diff --git a/src/core/remoteFileSystemProvider.ts b/src/core/remoteFileSystemProvider.ts index 87a4d4ec..129f47f0 100644 --- a/src/core/remoteFileSystemProvider.ts +++ b/src/core/remoteFileSystemProvider.ts @@ -885,15 +885,18 @@ export class VirtualFileSystem extends vscode.Disposable { } // compile project const res = await this.api.compile(identity, this.projectId, rootDocId??this.root?.rootDoc_id??null, draft, stopOnFirstError); - if (res.type==='success' && res.compile?.status==='success') { - this.updateOutputs(res.compile.outputFiles); - return true; - } else { - if (res.message!==undefined) { - console.error('Compile failure.', res.message); + if (res.type==='success' && res.compile) { + const outputFiles = res.compile.outputFiles ?? []; + if (outputFiles.length > 0) { + this.updateOutputs(outputFiles); + return true; } - return false; } + + if (res.message!==undefined) { + console.error('Compile failure.', res.message); + } + return false; } return Promise.resolve(undefined); }