Skip to content

Commit 923c6c9

Browse files
authored
Compile asynchronously (#890)
1 parent 2cde6c1 commit 923c6c9

File tree

2 files changed

+175
-23
lines changed

2 files changed

+175
-23
lines changed

src/api/index.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
schemas,
1717
checkingConnection,
1818
} from "../extension";
19-
import { currentWorkspaceFolder, outputConsole } from "../utils";
19+
import { currentWorkspaceFolder, outputChannel, outputConsole } from "../utils";
2020

2121
const DEFAULT_API_VERSION = 1;
2222
import * as Atelier from "./atelier";
@@ -252,6 +252,7 @@ export class AtelierAPI {
252252
if (minVersion > apiVersion) {
253253
return Promise.reject(`${path} not supported by API version ${apiVersion}`);
254254
}
255+
const originalPath = path;
255256
if (minVersion && minVersion > 0) {
256257
path = `v${apiVersion}/${path}`;
257258
}
@@ -404,6 +405,18 @@ export class AtelierAPI {
404405
// The request errored out, but didn't give us an error string back
405406
throw { statusCode: response.status, message: response.statusText, errorText: "" };
406407
}
408+
409+
// Handle headers for the /work endpoints by storing the header values in the result object
410+
if (originalPath && originalPath.endsWith("/work") && method == "POST") {
411+
// This is a POST /work request, so we need to get the Location header
412+
data.result.location = response.headers.get("Location");
413+
} else if (originalPath && /^[^/]+\/work\/[^/]+$/.test(originalPath)) {
414+
// This is a GET or DELETE /work request, so we need to check the Retry-After header
415+
if (response.headers.has("Retry-After")) {
416+
data.result.retryafter = response.headers.get("Retry-After");
417+
}
418+
}
419+
407420
return data;
408421
} catch (error) {
409422
if (error.code === "ECONNREFUSED") {
@@ -594,4 +607,88 @@ export class AtelierAPI {
594607
};
595608
return this.request(1, "GET", `%SYS/cspapps/${this.ns || ""}`, null, params);
596609
}
610+
611+
// v1+
612+
private queueAsync(request: any): Promise<Atelier.Response> {
613+
return this.request(1, "POST", `${this.ns}/work`, request);
614+
}
615+
616+
// v1+
617+
private pollAsync(id: string): Promise<Atelier.Response> {
618+
return this.request(1, "GET", `${this.ns}/work/${id}`);
619+
}
620+
621+
// v1+
622+
private cancelAsync(id: string): Promise<Atelier.Response> {
623+
return this.request(1, "DELETE", `${this.ns}/work/${id}`);
624+
}
625+
626+
/**
627+
* Calls `cancelAsync()` repeatedly until the cancellation is confirmed.
628+
* The wait time between requests is 1 second.
629+
*/
630+
private async verifiedCancel(id: string): Promise<Atelier.Response> {
631+
outputChannel.appendLine(
632+
"\nWARNING: Compilation was cancelled. Partially-compiled documents may result in unexpected behavior."
633+
);
634+
let cancelResp = await this.cancelAsync(id);
635+
while (cancelResp.result.retryafter) {
636+
await new Promise((resolve) => {
637+
setTimeout(resolve, 1000);
638+
});
639+
cancelResp = await this.cancelAsync(id);
640+
}
641+
return cancelResp;
642+
}
643+
644+
/**
645+
* Recursive function that calls `pollAsync()` repeatedly until we get a result or the user cancels the request.
646+
* The wait time between requests starts at 50ms and increases exponentially, with a max wait of 15 seconds.
647+
*/
648+
private async getAsyncResult(id: string, wait: number, token: vscode.CancellationToken): Promise<Atelier.Response> {
649+
const pollResp = await this.pollAsync(id);
650+
if (token.isCancellationRequested) {
651+
// The user cancelled the request, so cancel it on the server
652+
return this.verifiedCancel(id);
653+
}
654+
if (pollResp.result.retryafter) {
655+
await new Promise((resolve) => {
656+
setTimeout(resolve, wait);
657+
});
658+
if (token.isCancellationRequested) {
659+
// The user cancelled the request, so cancel it on the server
660+
return this.verifiedCancel(id);
661+
}
662+
return this.getAsyncResult(id, wait < 10000 ? wait ** 1.075 : 15000, token);
663+
}
664+
return pollResp;
665+
}
666+
667+
/**
668+
* Use the undocumented /work endpoints to compile `docs` asynchronously.
669+
*/
670+
public async asyncCompile(
671+
docs: string[],
672+
token: vscode.CancellationToken,
673+
flags?: string,
674+
source = false
675+
): Promise<Atelier.Response> {
676+
// Queue the compile request
677+
return this.queueAsync({
678+
request: "compile",
679+
documents: docs.map((doc) => this.transformNameIfCsp(doc)),
680+
source,
681+
flags,
682+
}).then((queueResp) => {
683+
// Request was successfully queued, so get the ID
684+
const id: string = queueResp.result.location;
685+
if (token.isCancellationRequested) {
686+
// The user cancelled the request, so cancel it on the server
687+
return this.verifiedCancel(id);
688+
}
689+
690+
// Poll until we get a result or the user cancels the request
691+
return this.getAsyncResult(id, 50, token);
692+
});
693+
}
597694
}

src/commands/compile.ts

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
} from "../utils";
2525
import { PackageNode } from "../explorer/models/packageNode";
2626
import { NodeBase } from "../explorer/models/nodeBase";
27+
import { RootNode } from "../explorer/models/rootNode";
2728

2829
async function compileFlags(): Promise<string> {
2930
const defaultFlags = config().compileFlags;
@@ -180,7 +181,7 @@ function updateOthers(others: string[], baseUri: vscode.Uri) {
180181
dotParts.length <= partsToConvert
181182
? uri.path
182183
: dotParts.slice(0, partsToConvert).join("/") + "." + dotParts.slice(partsToConvert).join(".");
183-
console.log(`updateOthers: uri.path=${uri.path} baseUri.path=${baseUri.path} correctPath=${correctPath}`);
184+
//console.log(`updateOthers: uri.path=${uri.path} baseUri.path=${baseUri.path} correctPath=${correctPath}`);
184185
fileSystemProvider.fireFileChanged(uri.with({ path: correctPath }));
185186
} else {
186187
documentContentProvider.update(uri);
@@ -222,26 +223,27 @@ async function compile(docs: CurrentFile[], flags?: string): Promise<any> {
222223
return vscode.window
223224
.withProgress(
224225
{
225-
cancellable: false,
226+
cancellable: true,
226227
location: vscode.ProgressLocation.Notification,
227228
title: `Compiling: ${docs.length === 1 ? docs.map((el) => el.name).join(", ") : docs.length + " files"}`,
228229
},
229-
() =>
230+
(progress, token: vscode.CancellationToken) =>
230231
api
231-
.actionCompile(
232+
.asyncCompile(
232233
docs.map((el) => el.name),
234+
token,
233235
flags
234236
)
235237
.then((data) => {
236238
const info = docs.length > 1 ? "" : `${docs[0].name}: `;
237239
if (data.status && data.status.errors && data.status.errors.length) {
238240
throw new Error(`${info}Compile error`);
239241
} else if (!config("suppressCompileMessages")) {
240-
vscode.window.showInformationMessage(`${info}Compilation succeeded`, "Hide");
242+
vscode.window.showInformationMessage(`${info}Compilation succeeded.`, "Dismiss");
241243
}
242244
return docs;
243245
})
244-
.catch((error: Error) => {
246+
.catch(() => {
245247
if (!config("suppressCompileErrorMessages")) {
246248
vscode.window
247249
.showErrorMessage(
@@ -255,7 +257,7 @@ async function compile(docs: CurrentFile[], flags?: string): Promise<any> {
255257
}
256258
});
257259
}
258-
// Even when compile failed we should still fetch server changes
260+
// Always fetch server changes, even when compile failed or got cancelled
259261
return docs;
260262
})
261263
)
@@ -344,7 +346,7 @@ export async function namespaceCompile(askFlags = false): Promise<any> {
344346
throw new Error(`No Active Connection`);
345347
}
346348
const confirm = await vscode.window.showWarningMessage(
347-
`Compiling all files in namespace '${api.ns}' might be expensive. Are you sure you want to proceed?`,
349+
`Compiling all files in namespace ${api.ns} might be expensive. Are you sure you want to proceed?`,
348350
"Cancel",
349351
"Confirm"
350352
);
@@ -360,21 +362,40 @@ export async function namespaceCompile(askFlags = false): Promise<any> {
360362
}
361363
vscode.window.withProgress(
362364
{
363-
cancellable: false,
365+
cancellable: true,
364366
location: vscode.ProgressLocation.Notification,
365-
title: `Compiling Namespace: ${api.ns}`,
367+
title: `Compiling namespace ${api.ns}`,
366368
},
367-
async () => {
368-
const data = await api.actionCompile(fileTypes, flags);
369-
if (data.status && data.status.errors && data.status.errors.length) {
370-
// console.error(data.status.summary);
371-
throw new Error(`Compiling Namespace: ${api.ns} Error`);
372-
} else {
373-
vscode.window.showInformationMessage(`Compiling Namespace: ${api.ns} Success`);
374-
}
375-
const file = currentFile();
376-
return loadChanges([file]);
377-
}
369+
(progress, token: vscode.CancellationToken) =>
370+
api
371+
.asyncCompile(fileTypes, token, flags)
372+
.then((data) => {
373+
if (data.status && data.status.errors && data.status.errors.length) {
374+
throw new Error(`Compiling Namespace: ${api.ns} Error`);
375+
} else if (!config("suppressCompileMessages")) {
376+
vscode.window.showInformationMessage(`Compiling namespace ${api.ns} succeeded.`, "Dismiss");
377+
}
378+
})
379+
.catch(() => {
380+
if (!config("suppressCompileErrorMessages")) {
381+
vscode.window
382+
.showErrorMessage(
383+
`Compiling namespace ${api.ns} failed. Check 'ObjectScript' output channel for details.`,
384+
"Show",
385+
"Dismiss"
386+
)
387+
.then((action) => {
388+
if (action === "Show") {
389+
outputChannel.show(true);
390+
}
391+
});
392+
}
393+
})
394+
.then(() => {
395+
// Always fetch server changes, even when compile failed or got cancelled
396+
const file = currentFile();
397+
return loadChanges([file]);
398+
})
378399
);
379400
}
380401

@@ -442,9 +463,43 @@ export async function compileExplorerItems(nodes: NodeBase[]): Promise<any> {
442463
docs.push(node.fullName + ".*.cls");
443464
break;
444465
}
466+
} else if (node instanceof RootNode && node.contextValue === "dataNode:cspApplication") {
467+
docs.push(node.fullName + "/*");
445468
} else {
446469
docs.push(node.fullName);
447470
}
448471
}
449-
return api.actionCompile(docs, flags);
472+
return vscode.window.withProgress(
473+
{
474+
cancellable: true,
475+
location: vscode.ProgressLocation.Notification,
476+
title: `Compiling ${nodes.length === 1 ? nodes[0].fullName : nodes.length + " nodes"}`,
477+
},
478+
(progress, token: vscode.CancellationToken) =>
479+
api
480+
.asyncCompile(docs, token, flags)
481+
.then((data) => {
482+
const info = nodes.length > 1 ? "" : `${nodes[0].fullName}: `;
483+
if (data.status && data.status.errors && data.status.errors.length) {
484+
throw new Error(`${info}Compile error`);
485+
} else if (!config("suppressCompileMessages")) {
486+
vscode.window.showInformationMessage(`${info}Compilation succeeded.`, "Dismiss");
487+
}
488+
})
489+
.catch(() => {
490+
if (!config("suppressCompileErrorMessages")) {
491+
vscode.window
492+
.showErrorMessage(
493+
`Compilation failed. Check 'ObjectScript' output channel for details.`,
494+
"Show",
495+
"Dismiss"
496+
)
497+
.then((action) => {
498+
if (action === "Show") {
499+
outputChannel.show(true);
500+
}
501+
});
502+
}
503+
})
504+
);
450505
}

0 commit comments

Comments
 (0)