texlite 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{GitDialog-W3hLOWrF.js → GitDialog-CqGyUxfP.js} +1 -1
- package/dist/client/assets/{HistoryDialog-7ADYJcGi.js → HistoryDialog-BMl2zxBa.js} +1 -1
- package/dist/client/assets/{LatexEditor-ChHQwJXZ.js → LatexEditor-C4bFVwJZ.js} +22 -22
- package/dist/client/assets/{PdfPreview-CuvWphzY.js → PdfPreview-BpP2LnX7.js} +1 -1
- package/dist/client/assets/ProjectNavigationDialogs-B7mxG8Fz.js +1 -0
- package/dist/client/assets/{SystemMetricsDialog-C0m9JUDy.js → SystemMetricsDialog-BK_K3ah8.js} +1 -1
- package/dist/client/assets/index-B9n8hfV0.js +64 -0
- package/dist/client/assets/{index-DzcH7hBu.css → index-BhyTKMoO.css} +1 -1
- package/dist/client/assets/{minus-BfqMIeUg.js → minus-BwzSZenU.js} +1 -1
- package/dist/client/assets/{spellCheck-BBnCnxmQ.js → spellCheck-CvOAlLWp.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/collaboration.js +33 -9
- package/dist/server/compiler.js +139 -10
- package/dist/server/routes/compile.js +54 -41
- package/dist/server/routes/projectFiles.js +11 -0
- package/package.json +1 -1
- package/dist/client/assets/ProjectNavigationDialogs-CzFhIw6B.js +0 -1
- package/dist/client/assets/index-DRShM2ob.js +0 -64
package/dist/server/compiler.js
CHANGED
|
@@ -9,6 +9,13 @@ import { gunzipSync, gzipSync } from "node:zlib";
|
|
|
9
9
|
import { listProjectFilesAsync, outputRoot, resolveSourcePath, safeRelativePath, texFileStem } from "./files.js";
|
|
10
10
|
import { parseCompileDiagnostics } from "./compileDiagnostics.js";
|
|
11
11
|
import { detachedProcessGroup, killProcessGroup } from "./processTree.js";
|
|
12
|
+
/** A controlled stop is a normal compile outcome, never a server error. */
|
|
13
|
+
export class CompileCancelledError extends Error {
|
|
14
|
+
constructor() {
|
|
15
|
+
super("Compilation was cancelled.");
|
|
16
|
+
this.name = "CompileCancelledError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
12
19
|
export class CompileQueue {
|
|
13
20
|
concurrency;
|
|
14
21
|
running = 0;
|
|
@@ -16,16 +23,39 @@ export class CompileQueue {
|
|
|
16
23
|
constructor(concurrency) {
|
|
17
24
|
this.concurrency = concurrency;
|
|
18
25
|
}
|
|
19
|
-
add(job) {
|
|
26
|
+
add(job, signal) {
|
|
20
27
|
return new Promise((resolve, reject) => {
|
|
21
|
-
|
|
28
|
+
let started = false;
|
|
29
|
+
const cancelPending = () => {
|
|
30
|
+
if (started)
|
|
31
|
+
return;
|
|
32
|
+
const index = this.pending.indexOf(task);
|
|
33
|
+
if (index < 0)
|
|
34
|
+
return;
|
|
35
|
+
this.pending.splice(index, 1);
|
|
36
|
+
reject(new CompileCancelledError());
|
|
37
|
+
this.drain();
|
|
38
|
+
};
|
|
39
|
+
const task = async () => {
|
|
40
|
+
started = true;
|
|
41
|
+
signal?.removeEventListener("abort", cancelPending);
|
|
42
|
+
if (signal?.aborted) {
|
|
43
|
+
reject(new CompileCancelledError());
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
22
46
|
try {
|
|
23
47
|
resolve(await job());
|
|
24
48
|
}
|
|
25
49
|
catch (error) {
|
|
26
50
|
reject(error);
|
|
27
51
|
}
|
|
28
|
-
}
|
|
52
|
+
};
|
|
53
|
+
if (signal?.aborted) {
|
|
54
|
+
reject(new CompileCancelledError());
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.pending.push(task);
|
|
58
|
+
signal?.addEventListener("abort", cancelPending, { once: true });
|
|
29
59
|
this.drain();
|
|
30
60
|
});
|
|
31
61
|
}
|
|
@@ -94,8 +124,42 @@ export class ProjectCompileCoordinator {
|
|
|
94
124
|
}
|
|
95
125
|
return managed.promise;
|
|
96
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Stop the active target and discard any newer pending request for it. A
|
|
129
|
+
* target has one writer-visible compile at a time, so cancelling the whole
|
|
130
|
+
* target avoids immediately starting a stale follow-up compilation.
|
|
131
|
+
*/
|
|
132
|
+
cancel(projectId, target) {
|
|
133
|
+
const targetKey = `${projectId}\0${target ?? ""}`;
|
|
134
|
+
const state = this.targets.get(targetKey);
|
|
135
|
+
const active = state?.active;
|
|
136
|
+
if (!active)
|
|
137
|
+
return null;
|
|
138
|
+
const status = active.started ? "running" : "queued";
|
|
139
|
+
active.controller.abort();
|
|
140
|
+
if (state?.pending) {
|
|
141
|
+
const pending = state.pending;
|
|
142
|
+
state.pending = null;
|
|
143
|
+
pending.controller.abort();
|
|
144
|
+
pending.input.onDiscarded("cancelled");
|
|
145
|
+
pending.resolve(cancelledCompileResult(pending.input));
|
|
146
|
+
}
|
|
147
|
+
return { runId: active.input.runId, status };
|
|
148
|
+
}
|
|
97
149
|
start(targetKey, state, job) {
|
|
98
|
-
void this.queue.add(
|
|
150
|
+
void this.queue.add(async () => {
|
|
151
|
+
job.started = true;
|
|
152
|
+
if (job.controller.signal.aborted)
|
|
153
|
+
throw new CompileCancelledError();
|
|
154
|
+
return await job.input.execute(job.controller.signal);
|
|
155
|
+
}, job.controller.signal).then(job.resolve, (error) => {
|
|
156
|
+
if (error instanceof CompileCancelledError) {
|
|
157
|
+
job.input.onCancelled?.();
|
|
158
|
+
job.resolve(cancelledCompileResult(job.input));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
job.reject(error);
|
|
162
|
+
}).finally(() => {
|
|
99
163
|
if (state.active !== job)
|
|
100
164
|
return;
|
|
101
165
|
state.active = null;
|
|
@@ -122,7 +186,19 @@ function deferredJob(input) {
|
|
|
122
186
|
resolve = resolvePromise;
|
|
123
187
|
reject = rejectPromise;
|
|
124
188
|
});
|
|
125
|
-
return { input, promise, resolve, reject };
|
|
189
|
+
return { input, promise, resolve, reject, controller: new AbortController(), started: false };
|
|
190
|
+
}
|
|
191
|
+
function cancelledCompileResult(input) {
|
|
192
|
+
return {
|
|
193
|
+
runId: input.runId,
|
|
194
|
+
revision: input.revision,
|
|
195
|
+
ok: false,
|
|
196
|
+
cancelled: true,
|
|
197
|
+
log: new CompileCancelledError().message,
|
|
198
|
+
diagnostics: { warnings: [], errors: [] },
|
|
199
|
+
pdfPath: null,
|
|
200
|
+
synctexPath: null
|
|
201
|
+
};
|
|
126
202
|
}
|
|
127
203
|
export async function captureCompileSnapshot(config, projectId, runId, settings) {
|
|
128
204
|
const root = compileRunRoot(config, projectId, runId);
|
|
@@ -532,7 +608,10 @@ function rewriteSynctexSource(synctexPath, cacheSource, snapshotSource) {
|
|
|
532
608
|
if (rewritten !== original)
|
|
533
609
|
fs.writeFileSync(synctexPath, gzipSync(Buffer.from(rewritten)), { mode: 0o600 });
|
|
534
610
|
}
|
|
535
|
-
export async function compileProject(config, snapshot, mainFileInput, engine, latexmkrcInput) {
|
|
611
|
+
export async function compileProject(config, snapshot, mainFileInput, engine, latexmkrcInput, options = {}) {
|
|
612
|
+
const { signal } = options;
|
|
613
|
+
if (signal?.aborted)
|
|
614
|
+
return cancelledCompileResultForProject();
|
|
536
615
|
const mainFile = safeRelativePath(mainFileInput);
|
|
537
616
|
if (!/\.tex$/i.test(mainFile))
|
|
538
617
|
throw new Error("The main file must have a .tex extension");
|
|
@@ -548,6 +627,10 @@ export async function compileProject(config, snapshot, mainFileInput, engine, la
|
|
|
548
627
|
const cacheStartedAt = performance.now();
|
|
549
628
|
const cache = prepareCompileCache(config, snapshot, mainFile, engine, latexmkrc);
|
|
550
629
|
const cacheSyncMs = performance.now() - cacheStartedAt;
|
|
630
|
+
if (signal?.aborted)
|
|
631
|
+
return cancelledCompileResultForProject({
|
|
632
|
+
cacheSyncMs, latexmkMs: 0, artifactCopyMs: 0, totalMs: performance.now() - startedAt
|
|
633
|
+
});
|
|
551
634
|
const cwd = cache.sourceDir;
|
|
552
635
|
const outDir = cache.outputDir;
|
|
553
636
|
const engineFlag = engine === "xelatex" ? "-xelatex" : engine === "lualatex" ? "-lualatex" : "-pdf";
|
|
@@ -578,6 +661,7 @@ export async function compileProject(config, snapshot, mainFileInput, engine, la
|
|
|
578
661
|
stdio: ["ignore", "pipe", "pipe"]
|
|
579
662
|
});
|
|
580
663
|
let log = "";
|
|
664
|
+
let cancelled = false;
|
|
581
665
|
const append = (chunk) => {
|
|
582
666
|
log += chunk.toString("utf8");
|
|
583
667
|
if (log.length > 2_000_000)
|
|
@@ -585,19 +669,46 @@ export async function compileProject(config, snapshot, mainFileInput, engine, la
|
|
|
585
669
|
};
|
|
586
670
|
child.stdout.on("data", append);
|
|
587
671
|
child.stderr.on("data", append);
|
|
588
|
-
|
|
589
|
-
const
|
|
672
|
+
let timeout = null;
|
|
673
|
+
const cancel = () => {
|
|
674
|
+
if (cancelled)
|
|
675
|
+
return;
|
|
676
|
+
cancelled = true;
|
|
677
|
+
log += "\nCompilation was cancelled.\n";
|
|
678
|
+
killProcessGroup(child);
|
|
679
|
+
};
|
|
680
|
+
const removeAbortListener = () => signal?.removeEventListener("abort", cancel);
|
|
681
|
+
child.once("error", (error) => {
|
|
682
|
+
if (timeout)
|
|
683
|
+
clearTimeout(timeout);
|
|
684
|
+
removeAbortListener();
|
|
685
|
+
reject(error);
|
|
686
|
+
});
|
|
687
|
+
timeout = setTimeout(() => {
|
|
688
|
+
if (cancelled)
|
|
689
|
+
return;
|
|
590
690
|
log += `\nCompilation exceeded ${config.compileTimeoutMs / 1000} seconds and was terminated.\n`;
|
|
591
691
|
killProcessGroup(child);
|
|
592
692
|
}, config.compileTimeoutMs);
|
|
693
|
+
if (signal?.aborted)
|
|
694
|
+
cancel();
|
|
695
|
+
else
|
|
696
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
593
697
|
child.on("close", (code) => {
|
|
594
|
-
|
|
595
|
-
|
|
698
|
+
if (timeout)
|
|
699
|
+
clearTimeout(timeout);
|
|
700
|
+
removeAbortListener();
|
|
701
|
+
resolve({ code, log, cancelled });
|
|
596
702
|
});
|
|
597
703
|
});
|
|
598
704
|
const latexmkMs = performance.now() - latexmkStartedAt;
|
|
599
705
|
const basename = texFileStem(mainFile);
|
|
600
706
|
const cachedPdf = path.join(outDir, `${basename}.pdf`);
|
|
707
|
+
if (processResult.cancelled || signal?.aborted) {
|
|
708
|
+
return cancelledCompileResultForProject({
|
|
709
|
+
cacheSyncMs, latexmkMs, artifactCopyMs: 0, totalMs: performance.now() - startedAt
|
|
710
|
+
}, processResult.log);
|
|
711
|
+
}
|
|
601
712
|
const ok = processResult.code === 0 && fs.existsSync(cachedPdf);
|
|
602
713
|
const diagnostics = parseCompileDiagnostics(processResult.log, ok ? "succeeded" : "failed");
|
|
603
714
|
if (!ok) {
|
|
@@ -606,6 +717,10 @@ export async function compileProject(config, snapshot, mainFileInput, engine, la
|
|
|
606
717
|
timings: { cacheSyncMs, latexmkMs, artifactCopyMs: 0, totalMs: performance.now() - startedAt }
|
|
607
718
|
};
|
|
608
719
|
}
|
|
720
|
+
if (signal?.aborted)
|
|
721
|
+
return cancelledCompileResultForProject({
|
|
722
|
+
cacheSyncMs, latexmkMs, artifactCopyMs: 0, totalMs: performance.now() - startedAt
|
|
723
|
+
});
|
|
609
724
|
const artifactStartedAt = performance.now();
|
|
610
725
|
const artifacts = materializeCompileArtifacts(cache, snapshot, basename);
|
|
611
726
|
const artifactCopyMs = performance.now() - artifactStartedAt;
|
|
@@ -614,3 +729,17 @@ export async function compileProject(config, snapshot, mainFileInput, engine, la
|
|
|
614
729
|
timings: { cacheSyncMs, latexmkMs, artifactCopyMs, totalMs: performance.now() - startedAt }
|
|
615
730
|
};
|
|
616
731
|
}
|
|
732
|
+
function cancelledCompileResultForProject(timings, log = "") {
|
|
733
|
+
const message = log.includes("Compilation was cancelled.")
|
|
734
|
+
? log
|
|
735
|
+
: `${log}${log && !log.endsWith("\n") ? "\n" : ""}Compilation was cancelled.\n`;
|
|
736
|
+
return {
|
|
737
|
+
ok: false,
|
|
738
|
+
cancelled: true,
|
|
739
|
+
log: message,
|
|
740
|
+
diagnostics: parseCompileDiagnostics(message, "failed"),
|
|
741
|
+
pdfPath: null,
|
|
742
|
+
synctexPath: null,
|
|
743
|
+
...(timings ? { timings } : {})
|
|
744
|
+
};
|
|
745
|
+
}
|
|
@@ -5,7 +5,7 @@ import { performance } from "node:perf_hooks";
|
|
|
5
5
|
import { requireUser } from "../auth.js";
|
|
6
6
|
import { accessibleProject, canEdit } from "../projects.js";
|
|
7
7
|
import { resolveSourcePath, safeRelativePath, texFileStem } from "../files.js";
|
|
8
|
-
import { captureCompileSnapshot, cleanCompileArtifacts, cleanCompileCache, compileProject, discardCompileSnapshot, hasCompileCache, publishCompileArtifacts, publishedCompileArtifacts } from "../compiler.js";
|
|
8
|
+
import { captureCompileSnapshot, cleanCompileArtifacts, cleanCompileCache, CompileCancelledError, compileProject, discardCompileSnapshot, hasCompileCache, publishCompileArtifacts, publishedCompileArtifacts } from "../compiler.js";
|
|
9
9
|
import { parseCompileDiagnostics } from "../compileDiagnostics.js";
|
|
10
10
|
import { pdfToSource, sourceToPdf } from "../synctex.js";
|
|
11
11
|
import { availablePdf, compileRunPdf, compileMainFile, compilePdfUrl, isTextCompileArtifact, listCompileArtifacts, pdfLoadingMode, syncArtifacts } from "../compileArtifacts.js";
|
|
@@ -32,6 +32,10 @@ function pdfResponseMetadata(config, pdfPath) {
|
|
|
32
32
|
function mainDocumentChangedError() {
|
|
33
33
|
return httpError(409, "MAIN_DOCUMENT_CHANGED");
|
|
34
34
|
}
|
|
35
|
+
function throwIfCompileCancelled(signal) {
|
|
36
|
+
if (signal.aborted)
|
|
37
|
+
throw new CompileCancelledError();
|
|
38
|
+
}
|
|
35
39
|
class CompileSnapshotBusyError extends Error {
|
|
36
40
|
code = "COMPILE_SNAPSHOT_BUSY";
|
|
37
41
|
constructor() {
|
|
@@ -292,6 +296,21 @@ export function registerCompileRoutes(app, context) {
|
|
|
292
296
|
throw httpError(404, "PROJECT_NOT_FOUND");
|
|
293
297
|
} });
|
|
294
298
|
});
|
|
299
|
+
app.post("/api/projects/:id/compile/cancel", async (request, reply) => {
|
|
300
|
+
const user = requireUser(request, reply, db);
|
|
301
|
+
if (!user)
|
|
302
|
+
return;
|
|
303
|
+
const { id } = request.params;
|
|
304
|
+
const project = accessibleProject(db, id, user);
|
|
305
|
+
if (!project || !canEdit(project))
|
|
306
|
+
return apiError(reply, 403, "COMPILE_FORBIDDEN");
|
|
307
|
+
const body = (request.body ?? {});
|
|
308
|
+
const mainFile = compileMainFile(config, id, project.main_file, body.mainFile);
|
|
309
|
+
if (!mainFile)
|
|
310
|
+
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
|
|
311
|
+
const cancelled = compileCoordinator.cancel(id, mainFile);
|
|
312
|
+
return { cancelled: Boolean(cancelled), mainFile, runId: cancelled?.runId ?? null, status: cancelled?.status ?? null };
|
|
313
|
+
});
|
|
295
314
|
app.post("/api/projects/:id/compile", async (request, reply) => {
|
|
296
315
|
const requestStartedAt = performance.now();
|
|
297
316
|
const user = requireUser(request, reply, db);
|
|
@@ -363,8 +382,8 @@ export function registerCompileRoutes(app, context) {
|
|
|
363
382
|
if (currentGeneration !== snapshotGeneration)
|
|
364
383
|
snapshotStale = true;
|
|
365
384
|
};
|
|
366
|
-
const broadcast = () => collaboration.signalCompileState(id, {
|
|
367
|
-
mainFile, runId, status: phase, requestedBy, updatedAt: now(),
|
|
385
|
+
const broadcast = (stateRunId = runId) => collaboration.signalCompileState(id, {
|
|
386
|
+
mainFile, runId: stateRunId, status: phase, requestedBy, updatedAt: now(),
|
|
368
387
|
...(phase === "succeeded" && snapshotStale ? { stale: true } : {})
|
|
369
388
|
});
|
|
370
389
|
const result = await compileCoordinator.request({
|
|
@@ -380,19 +399,28 @@ export function registerCompileRoutes(app, context) {
|
|
|
380
399
|
onDiscarded: () => {
|
|
381
400
|
db.prepare("DELETE FROM compile_runs WHERE id = ? AND status = 'queued'").run(runId);
|
|
382
401
|
},
|
|
402
|
+
onCancelled: () => {
|
|
403
|
+
phase = "failed";
|
|
404
|
+
db.prepare("UPDATE compile_runs SET status = 'failed', log = ?, finished_at = ? WHERE id = ? AND status IN ('queued', 'running')")
|
|
405
|
+
.run(new CompileCancelledError().message, now(), runId);
|
|
406
|
+
broadcast();
|
|
407
|
+
},
|
|
383
408
|
// The compile reservation protects the shared compiler cache and
|
|
384
409
|
// prevents Git/cleanup/deletion from replacing the project while
|
|
385
410
|
// latexmk is running, but it deliberately does not occupy the ordinary
|
|
386
411
|
// project queue for the duration of the subprocess.
|
|
387
|
-
execute: async () => projectMutations.runCompile(id, async () => {
|
|
412
|
+
execute: async (signal) => projectMutations.runCompile(id, async () => {
|
|
388
413
|
let snapshot = null;
|
|
389
414
|
let snapshotMs = 0;
|
|
390
415
|
const snapshotStartedAt = performance.now();
|
|
391
416
|
try {
|
|
392
|
-
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
|
|
417
|
+
throwIfCompileCancelled(signal);
|
|
418
|
+
// Revalidate settings after queued source operations. Do not use the
|
|
419
|
+
// in-memory collaboration revision to skip before a snapshot: it is
|
|
420
|
+
// not a durable content version and can collide after a room restart
|
|
421
|
+
// or reconnect.
|
|
422
|
+
await projectMutations.runSerialized(id, () => {
|
|
423
|
+
throwIfCompileCancelled(signal);
|
|
396
424
|
const currentProject = accessibleProject(db, id, user);
|
|
397
425
|
if (!currentProject || !canEdit(currentProject)) {
|
|
398
426
|
throw new Error("Project access changed while waiting for a source snapshot");
|
|
@@ -404,38 +432,9 @@ export function registerCompileRoutes(app, context) {
|
|
|
404
432
|
throw mainDocumentChangedError();
|
|
405
433
|
project = currentProject;
|
|
406
434
|
mainFile = currentMainFile;
|
|
407
|
-
return {
|
|
408
|
-
generation: compileRequestGeneration(currentProject, collaboration.currentRevision(id), collaboration.currentGeneration(id), config.extraArgs)
|
|
409
|
-
};
|
|
410
435
|
});
|
|
411
|
-
const currentGeneration = refreshed.generation;
|
|
412
|
-
const publishedBeforeSnapshot = publishedCompileArtifacts(config, id, mainFile, mainFile === project.main_file);
|
|
413
|
-
const publishedRunBeforeSnapshot = publishedBeforeSnapshot ? db.prepare(`SELECT id, status, log, finished_at
|
|
414
|
-
FROM compile_runs WHERE id = ? AND project_id = ?`).get(publishedBeforeSnapshot.runId, id) : undefined;
|
|
415
|
-
const latestRunBeforeSnapshot = db.prepare(`SELECT id, status FROM compile_runs
|
|
416
|
-
WHERE project_id = ? AND main_file = ? AND id <> ? ORDER BY created_at DESC LIMIT 1`).get(id, mainFile, runId);
|
|
417
|
-
const activeRunBeforeSnapshot = db.prepare(`SELECT 1 AS active FROM compile_runs
|
|
418
|
-
WHERE project_id = ? AND main_file = ? AND id <> ? AND status IN ('queued', 'running') LIMIT 1`).get(id, mainFile, runId);
|
|
419
|
-
if (!activeRunBeforeSnapshot && publishedBeforeSnapshot?.generation === currentGeneration
|
|
420
|
-
&& publishedRunBeforeSnapshot?.status === "succeeded"
|
|
421
|
-
&& latestRunBeforeSnapshot?.id === publishedRunBeforeSnapshot.id
|
|
422
|
-
&& hasCompileCache(config, id, mainFile)) {
|
|
423
|
-
db.prepare("DELETE FROM compile_runs WHERE id = ? AND status = 'queued'").run(runId);
|
|
424
|
-
phase = "succeeded";
|
|
425
|
-
broadcast();
|
|
426
|
-
return {
|
|
427
|
-
runId: publishedBeforeSnapshot.runId,
|
|
428
|
-
ok: true,
|
|
429
|
-
skipped: true,
|
|
430
|
-
log: publishedRunBeforeSnapshot.log ?? "",
|
|
431
|
-
diagnostics: parseCompileDiagnostics(publishedRunBeforeSnapshot.log ?? "", "succeeded"),
|
|
432
|
-
pdfPath: null,
|
|
433
|
-
synctexPath: publishedBeforeSnapshot.synctex,
|
|
434
|
-
revision: publishedBeforeSnapshot.revision,
|
|
435
|
-
snapshotMs: 0
|
|
436
|
-
};
|
|
437
|
-
}
|
|
438
436
|
const captured = await projectMutations.runSerialized(id, async () => {
|
|
437
|
+
throwIfCompileCancelled(signal);
|
|
439
438
|
const currentProject = accessibleProject(db, id, user);
|
|
440
439
|
if (!currentProject || !canEdit(currentProject)) {
|
|
441
440
|
throw new Error("Project access changed while waiting for a source snapshot");
|
|
@@ -469,6 +468,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
469
468
|
extraArgs: config.extraArgs,
|
|
470
469
|
generation: generationKey
|
|
471
470
|
});
|
|
471
|
+
throwIfCompileCancelled(signal);
|
|
472
472
|
}
|
|
473
473
|
catch (error) {
|
|
474
474
|
captureError = error;
|
|
@@ -508,6 +508,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
508
508
|
throw new CompileSnapshotBusyError();
|
|
509
509
|
}, { flush: false });
|
|
510
510
|
snapshot = captured;
|
|
511
|
+
throwIfCompileCancelled(signal);
|
|
511
512
|
snapshotMs = performance.now() - snapshotStartedAt;
|
|
512
513
|
metrics.record("compile.snapshot", snapshotMs);
|
|
513
514
|
const published = publishedCompileArtifacts(config, id, mainFile, mainFile === project.main_file);
|
|
@@ -526,7 +527,9 @@ export function registerCompileRoutes(app, context) {
|
|
|
526
527
|
snapshot = null;
|
|
527
528
|
db.prepare("DELETE FROM compile_runs WHERE id = ? AND status = 'queued'").run(runId);
|
|
528
529
|
phase = "succeeded";
|
|
529
|
-
broadcast
|
|
530
|
+
// The probe row was deleted, so broadcast the retained run that
|
|
531
|
+
// clients can actually resolve from SQLite and the manifest.
|
|
532
|
+
broadcast(published.runId);
|
|
530
533
|
return {
|
|
531
534
|
runId: published.runId,
|
|
532
535
|
ok: true,
|
|
@@ -545,15 +548,17 @@ export function registerCompileRoutes(app, context) {
|
|
|
545
548
|
broadcast();
|
|
546
549
|
let compiled;
|
|
547
550
|
try {
|
|
548
|
-
compiled = await compileProject(config, snapshot, mainFile, project.engine, project.latexmkrc);
|
|
551
|
+
compiled = await compileProject(config, snapshot, mainFile, project.engine, project.latexmkrc, { signal });
|
|
549
552
|
refreshSnapshotStale();
|
|
550
553
|
if (compiled.ok && compiled.pdfPath) {
|
|
554
|
+
throwIfCompileCancelled(signal);
|
|
551
555
|
const publishStartedAt = performance.now();
|
|
552
556
|
// Publishing is short and atomic, but still goes through the
|
|
553
557
|
// ordinary project queue so it cannot overlap a source-tree
|
|
554
558
|
// archive/read or another output mutation. The long latexmk
|
|
555
559
|
// process above remains outside that queue.
|
|
556
560
|
await projectMutations.runSerialized(id, () => {
|
|
561
|
+
throwIfCompileCancelled(signal);
|
|
557
562
|
if (!db.prepare("SELECT 1 FROM projects WHERE id = ?").get(id)) {
|
|
558
563
|
throw new Error("Project was deleted before compile artifacts could be published.");
|
|
559
564
|
}
|
|
@@ -576,12 +581,19 @@ export function registerCompileRoutes(app, context) {
|
|
|
576
581
|
snapshot = null;
|
|
577
582
|
compiled = {
|
|
578
583
|
ok: false,
|
|
584
|
+
...(error instanceof CompileCancelledError ? { cancelled: true } : {}),
|
|
579
585
|
log: error instanceof Error ? error.message : String(error),
|
|
580
586
|
diagnostics: parseCompileDiagnostics(error instanceof Error ? error.message : String(error), "failed"),
|
|
581
587
|
pdfPath: null,
|
|
582
588
|
synctexPath: null
|
|
583
589
|
};
|
|
584
590
|
}
|
|
591
|
+
// A killed latexmk can leave an incomplete dependency database in
|
|
592
|
+
// the incremental cache. Keep the last published PDF, but make the
|
|
593
|
+
// next requested compile a clean rebuild instead of trusting that
|
|
594
|
+
// partial state.
|
|
595
|
+
if (compiled.cancelled)
|
|
596
|
+
cleanCompileCache(config, id, mainFile);
|
|
585
597
|
phase = compiled.ok ? "succeeded" : "failed";
|
|
586
598
|
db.prepare("UPDATE compile_runs SET status = ?, log = ?, finished_at = ? WHERE id = ?")
|
|
587
599
|
.run(phase, compiled.log, now(), runId);
|
|
@@ -617,6 +629,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
617
629
|
broadcast();
|
|
618
630
|
return {
|
|
619
631
|
ok: false,
|
|
632
|
+
...(error instanceof CompileCancelledError ? { cancelled: true } : {}),
|
|
620
633
|
log,
|
|
621
634
|
diagnostics: parseCompileDiagnostics(log, "failed"),
|
|
622
635
|
pdfPath: null,
|
|
@@ -660,7 +673,7 @@ export function registerCompileRoutes(app, context) {
|
|
|
660
673
|
`total;dur=${timingDuration(requestMs)}`
|
|
661
674
|
].filter(Boolean).join(", "));
|
|
662
675
|
return {
|
|
663
|
-
mainFile, runId: result.runId, ok: result.ok, skipped: result.skipped === true, log: result.log, diagnostics: result.diagnostics,
|
|
676
|
+
mainFile, runId: result.runId, ok: result.ok, cancelled: result.cancelled === true, skipped: result.skipped === true, log: result.log, diagnostics: result.diagnostics,
|
|
664
677
|
pdfUrl: result.ok ? compilePdfUrl(id, mainFile, result.runId) : null,
|
|
665
678
|
pdfCompiledAt: result.ok ? completed?.finished_at ?? null : null,
|
|
666
679
|
...pdfMetadata,
|
|
@@ -212,6 +212,14 @@ export function registerProjectFileRoutes(app, context) {
|
|
|
212
212
|
return apiError(reply, 404, "PATH_NOT_FOUND", { path: source });
|
|
213
213
|
if (destination === source)
|
|
214
214
|
return { ok: true, path: source };
|
|
215
|
+
// Moving the configured root to (for example) main.tex.bak used to
|
|
216
|
+
// silently store that non-TeX path as main_file. The next compilation
|
|
217
|
+
// then failed long after the rename had appeared to succeed. Keep the
|
|
218
|
+
// current root a TeX file; users can first select another root document
|
|
219
|
+
// in project settings when they genuinely want to retire this one.
|
|
220
|
+
if (source === project.main_file && !/\.tex$/i.test(destination)) {
|
|
221
|
+
return apiError(reply, 400, "MAIN_DOCUMENT_INVALID");
|
|
222
|
+
}
|
|
215
223
|
const initialSourceStat = fs.statSync(sourceAbsolute);
|
|
216
224
|
if (initialSourceStat.isDirectory() && (destinationDirectory === source || destinationDirectory.startsWith(`${source}/`))) {
|
|
217
225
|
return apiError(reply, 400, "MOVE_INTO_SELF", { path: source });
|
|
@@ -229,6 +237,9 @@ export function registerProjectFileRoutes(app, context) {
|
|
|
229
237
|
const validateMove = () => {
|
|
230
238
|
currentProject = requireEditableProject(db, id, user);
|
|
231
239
|
assertNoSourceSymlinks(config, id);
|
|
240
|
+
if (source === currentProject.main_file && !/\.tex$/i.test(destination)) {
|
|
241
|
+
throw httpError(400, "MAIN_DOCUMENT_INVALID");
|
|
242
|
+
}
|
|
232
243
|
if (!fs.existsSync(sourceAbsolute)) {
|
|
233
244
|
throw httpError(404, "PATH_NOT_FOUND", { path: source });
|
|
234
245
|
}
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{u as A,r,j as e}from"./react-BoEjKzFZ.js";import{j as B,a as R,M as F,o as $,L as I,p as J,n as K,F as V,q as G}from"./index-DRShM2ob.js";const L=B("Replace",[["path",{d:"M14 4a2 2 0 0 1 2-2",key:"1w2hp7"}],["path",{d:"M16 10a2 2 0 0 1-2-2",key:"shjach"}],["path",{d:"M20 2a2 2 0 0 1 2 2",key:"188mtx"}],["path",{d:"M22 8a2 2 0 0 1-2 2",key:"ddf4tu"}],["path",{d:"m3 7 3 3 3-3",key:"x25e72"}],["path",{d:"M6 10V5a3 3 0 0 1 3-3h1",key:"3y3t5z"}],["rect",{x:"2",y:"14",width:"8",height:"8",rx:"2",key:"4rksxw"}]]);function Z({open:i,files:u,onOpenChange:l,onOpenFile:p}){const{t:a}=A(),[n,m]=r.useState(""),[d,o]=r.useState(0),f=r.useRef(null),h=r.useMemo(()=>u.filter(t=>t.type==="file").map(t=>({entry:t,score:H(t.path,n)})).filter(t=>t.score>=0).sort((t,c)=>c.score-t.score||t.entry.path.localeCompare(c.entry.path)).slice(0,100),[u,n]);r.useEffect(()=>{i&&(m(""),o(0),window.setTimeout(()=>f.current?.focus(),0))},[i]),r.useEffect(()=>o(t=>Math.min(t,Math.max(0,h.length-1))),[h.length]);const x=t=>{p(t),l(!1)};return e.jsx(F,{open:i,wide:!0,title:a("navigation.quickOpen"),description:a("navigation.quickOpenDescription"),onOpenChange:l,children:e.jsxs("div",{className:"quick-open-dialog",children:[e.jsxs("label",{children:[e.jsx($,{size:16}),e.jsx("input",{ref:f,value:n,placeholder:a("navigation.filePlaceholder"),onChange:t=>{m(t.target.value),o(0)},onKeyDown:t=>{t.key==="ArrowDown"&&(t.preventDefault(),o(c=>Math.min(h.length-1,c+1))),t.key==="ArrowUp"&&(t.preventDefault(),o(c=>Math.max(0,c-1))),t.key==="Enter"&&h[d]&&(t.preventDefault(),x(h[d].entry.path))}})]}),e.jsxs("div",{className:"quick-open-results",children:[h.map(({entry:t},c)=>e.jsxs("button",{className:c===d?"active":"",onMouseEnter:()=>o(c),onClick:()=>x(t.path),children:[e.jsx(V,{size:14}),e.jsx("span",{children:t.path}),c===d&&e.jsx(G,{size:13})]},t.path)),h.length===0&&e.jsx("p",{className:"muted padded",children:a("navigation.noFiles")})]})]})})}function _({open:i,project:u,onOpenChange:l,onJump:p}){const{t:a}=A(),[n,m]=r.useState(""),[d,o]=r.useState(""),[f,h]=r.useState(!1),[x,t]=r.useState(!1),[c,w]=r.useState([]),[v,C]=r.useState(0),[P,D]=r.useState(!1),[b,S]=r.useState(!1),[W,N]=r.useState(!1),[q,M]=r.useState(""),[z,k]=r.useState(""),[O,Q]=r.useState(0),y=r.useRef(null);r.useEffect(()=>{if(!i){y.current?.abort();return}if(M(""),k(""),!n.trim()){w([]),C(0),D(!1);return}const s=window.setTimeout(()=>{y.current?.abort();const j=new AbortController;y.current=j,S(!0);const E=new URLSearchParams({q:n});f&&E.set("caseSensitive","1"),x&&E.set("wholeWord","1"),R(`/api/projects/${u.id}/search?${E}`,{signal:j.signal}).then(g=>{w(g.matches),C(g.total),D(g.truncated)}).catch(g=>{g instanceof DOMException&&g.name==="AbortError"||k(T(g))}).finally(()=>{y.current===j&&(y.current=null,S(!1))})},250);return()=>window.clearTimeout(s)},[i,u.id,n,f,x,O]);const U=async()=>{N(!1),S(!0),k(""),M("");try{const s=await R(`/api/projects/${u.id}/search/replace`,{method:"POST",body:JSON.stringify({query:n,replacement:d,caseSensitive:f,wholeWord:x})});M(a("navigation.replaced",{count:s.replacements,files:s.files.length})),w([]),C(0),Q(j=>j+1)}catch(s){k(T(s))}finally{S(!1)}};return e.jsxs(e.Fragment,{children:[e.jsx(F,{open:i,extraWide:!0,title:a("navigation.projectSearch"),description:a("navigation.projectSearchDescription"),onOpenChange:l,footer:e.jsx("button",{onClick:()=>l(!1),children:a("common.close")}),children:e.jsxs("div",{className:"project-search-dialog",children:[z&&e.jsx("p",{className:"error",children:z}),q&&e.jsx("p",{className:"success",children:q}),e.jsxs("div",{className:"project-search-form",children:[e.jsxs("label",{className:"project-search-query",children:[e.jsx($,{size:16}),e.jsx("input",{autoFocus:!0,value:n,placeholder:a("navigation.searchPlaceholder"),onChange:s=>m(s.target.value)})]}),u.permission!=="read"&&e.jsxs("label",{className:"project-search-query",children:[e.jsx(L,{size:16}),e.jsx("input",{value:d,placeholder:a("navigation.replacePlaceholder"),onChange:s=>o(s.target.value)})]}),e.jsxs("div",{className:"project-search-options",children:[e.jsxs("label",{children:[e.jsx("input",{type:"checkbox",checked:f,onChange:s=>h(s.target.checked)}),a("editor.search.matchCase")]}),e.jsxs("label",{children:[e.jsx("input",{type:"checkbox",checked:x,onChange:s=>t(s.target.checked)}),a("editor.search.wholeWord")]}),u.permission!=="read"&&e.jsxs("button",{className:"danger-text",disabled:!n.trim()||v===0||b,onClick:()=>N(!0),children:[e.jsx(L,{size:13}),a("navigation.replaceAll",{count:v})]})]})]}),e.jsxs("div",{className:"project-search-summary",children:[e.jsx("span",{children:b?e.jsxs(e.Fragment,{children:[e.jsx(I,{className:"spin",size:13}),a("common.loading")]}):a("navigation.matchSummary",{count:v})}),P&&e.jsx("small",{children:a("navigation.truncated",{count:c.length})})]}),e.jsxs("div",{className:"project-search-results",children:[c.map((s,j)=>e.jsxs("button",{onClick:()=>{p(s.path,s.line,s.column),l(!1)},children:[e.jsx(J,{size:14}),e.jsxs("span",{children:[e.jsxs("strong",{children:[s.path,e.jsxs("small",{children:[":",s.line,":",s.column]})]}),e.jsxs("code",{children:[s.preview.slice(0,s.matchStart),e.jsx("mark",{children:s.preview.slice(s.matchStart,s.matchEnd)}),s.preview.slice(s.matchEnd)]})]})]},`${s.path}-${s.line}-${s.column}-${j}`)),!b&&n.trim()&&c.length===0&&e.jsx("p",{className:"muted padded",children:a("editor.search.noMatches")})]})]})}),e.jsx(K,{open:W,title:a("navigation.replaceConfirmTitle"),description:a("navigation.replaceConfirmDescription",{count:v}),confirmLabel:a("navigation.replaceAll",{count:v}),danger:!0,onCancel:()=>N(!1),onConfirm:()=>{U()}})]})}function H(i,u){const l=i.toLocaleLowerCase(),p=u.trim().toLocaleLowerCase();if(!p)return 1;const a=l.indexOf(p);if(a>=0)return 1e4-a-l.length;let n=0,m=0;for(const d of p){const o=l.indexOf(d,n);if(o<0)return-1;m+=o===n?5:1,n=o+1}return m-l.length/100}function T(i){return i instanceof Error?i.message:String(i)}export{_ as ProjectSearchDialog,Z as QuickOpenDialog};
|