@mcmcjs/julia 0.6.1 → 0.7.0

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/index.cjs CHANGED
@@ -28,6 +28,7 @@ __export(index_exports, {
28
28
  gcVersions: () => gcVersions,
29
29
  juliaEngine: () => juliaEngine,
30
30
  juliaupInstallCommand: () => juliaupInstallCommand,
31
+ killWorker: () => killWorker,
31
32
  listVersions: () => listVersions,
32
33
  listWorkers: () => listWorkers,
33
34
  managedProjectDir: () => managedProjectDir,
@@ -110,12 +111,13 @@ var juliaEngine = {
110
111
  };
111
112
 
112
113
  // src/fit.ts
113
- var import_node_fs = require("fs");
114
+ var import_node_fs2 = require("fs");
114
115
  var import_node_path3 = require("path");
115
116
  var import_core = require("@mcmcjs/core");
116
117
 
117
118
  // src/runner-common.ts
118
119
  var import_node_crypto = require("crypto");
120
+ var import_node_fs = require("fs");
119
121
  var import_node_os2 = require("os");
120
122
  var import_node_path2 = require("path");
121
123
  var import_node_url = require("url");
@@ -123,11 +125,19 @@ function sharedTmpParent() {
123
125
  const uid = typeof process.getuid === "function" ? `-${process.getuid()}` : "";
124
126
  return (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `mcmcjs${uid}`);
125
127
  }
128
+ function driverFile(name) {
129
+ const here = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl));
130
+ const beside = (0, import_node_path2.join)(here, name);
131
+ return (0, import_node_fs.existsSync)(beside) ? beside : (0, import_node_path2.join)(here, "driver", name);
132
+ }
126
133
  function driverPath() {
127
- return (0, import_node_path2.join)((0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "driver.jl");
134
+ return driverFile("driver.jl");
128
135
  }
129
136
  function workerPath() {
130
- return (0, import_node_path2.join)((0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "worker.jl");
137
+ return driverFile("worker.jl");
138
+ }
139
+ function pinnedEnvDir() {
140
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "julia-env");
131
141
  }
132
142
  function sha256(text) {
133
143
  return (0, import_node_crypto.createHash)("sha256").update(text).digest("hex");
@@ -160,10 +170,10 @@ function toStage(value) {
160
170
  // src/fit.ts
161
171
  function makeRequestDir(kind) {
162
172
  const parent = sharedTmpParent();
163
- (0, import_node_fs.mkdirSync)(parent, { recursive: true });
164
- return (0, import_node_fs.mkdtempSync)((0, import_node_path3.join)(parent, `${kind}-`));
173
+ (0, import_node_fs2.mkdirSync)(parent, { recursive: true });
174
+ return (0, import_node_fs2.mkdtempSync)((0, import_node_path3.join)(parent, `${kind}-`));
165
175
  }
166
- function fitRequest(spec, outPath) {
176
+ function fitRequest(spec, outPath, opts) {
167
177
  return {
168
178
  schema_version: spec.schema_version,
169
179
  backend: { id: spec.backend.id },
@@ -171,12 +181,13 @@ function fitRequest(spec, outPath) {
171
181
  data: spec.data,
172
182
  sampler: spec.sampler,
173
183
  seed: spec.seed,
174
- out: outPath
184
+ out: outPath,
185
+ ...opts?.streamDraws ? { stream_draws: true } : {}
175
186
  };
176
187
  }
177
188
  function finalizeOkFit(spec, resolved, io, provenance, startedAt, elapsedMs) {
178
189
  const runtimeRequested = spec.backend.version;
179
- if (!(0, import_node_fs.existsSync)(io.outPath)) {
190
+ if (!(0, import_node_fs2.existsSync)(io.outPath)) {
180
191
  return {
181
192
  status: "error",
182
193
  runtimeRequested,
@@ -186,7 +197,7 @@ function finalizeOkFit(spec, resolved, io, provenance, startedAt, elapsedMs) {
186
197
  };
187
198
  }
188
199
  try {
189
- (0, import_core.parseSamples)((0, import_node_fs.readFileSync)(io.outPath, "utf8"));
200
+ (0, import_core.parseSamples)((0, import_node_fs2.readFileSync)(io.outPath, "utf8"));
190
201
  } catch (error) {
191
202
  return {
192
203
  status: "error",
@@ -205,14 +216,14 @@ function finalizeOkFit(spec, resolved, io, provenance, startedAt, elapsedMs) {
205
216
  runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
206
217
  manifest_sha256: provenance?.manifest_sha256,
207
218
  packages: provenance?.packages,
208
- model_sha256: (0, import_node_fs.existsSync)(spec.modelPath) ? sha256((0, import_node_fs.readFileSync)(spec.modelPath, "utf8")) : void 0,
219
+ model_sha256: (0, import_node_fs2.existsSync)(spec.modelPath) ? sha256((0, import_node_fs2.readFileSync)(spec.modelPath, "utf8")) : void 0,
209
220
  data_sha256: io.dataSha256 ?? sha256((0, import_core.canonicalJson)(spec.data)),
210
221
  ...io.dataFile ? { data_file: io.dataFile } : {},
211
222
  samples_file: io.outPath,
212
223
  started_at: startedAt,
213
224
  elapsed_ms: elapsedMs
214
225
  };
215
- (0, import_node_fs.writeFileSync)(io.recordPath ?? `${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
226
+ (0, import_node_fs2.writeFileSync)(io.recordPath ?? `${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
216
227
  `);
217
228
  return {
218
229
  status: "ok",
@@ -226,10 +237,16 @@ async function runFit(spec, resolved, io) {
226
237
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
227
238
  const start = performance.now();
228
239
  const runtimeRequested = spec.backend.version;
240
+ if (io.signal?.aborted) {
241
+ return { status: "cancelled", runtimeRequested, elapsedMs: 0 };
242
+ }
229
243
  const ownTmp = io.tmpDir === void 0;
230
244
  const tmp = io.tmpDir ?? makeRequestDir("fit");
231
245
  const requestPath = (0, import_node_path3.join)(tmp, "request.json");
232
- (0, import_node_fs.writeFileSync)(requestPath, JSON.stringify(fitRequest(spec, io.outPath)));
246
+ (0, import_node_fs2.writeFileSync)(
247
+ requestPath,
248
+ JSON.stringify(fitRequest(spec, io.outPath, { streamDraws: io.onDraws !== void 0 }))
249
+ );
233
250
  const args = [
234
251
  ...resolved.args,
235
252
  "--startup-file=no",
@@ -240,15 +257,21 @@ async function runFit(spec, resolved, io) {
240
257
  let stdout;
241
258
  let stderr;
242
259
  let code;
260
+ let cancelled;
243
261
  try {
244
- ({ stdout, stderr, code } = await io.spawn(resolved.command, args, {
245
- onProgress: io.onProgress
262
+ ({ stdout, stderr, code, cancelled } = await io.spawn(resolved.command, args, {
263
+ onProgress: io.onProgress,
264
+ onDraws: io.onDraws,
265
+ signal: io.signal
246
266
  }));
247
267
  } finally {
248
- if (ownTmp) (0, import_node_fs.rmSync)(tmp, { recursive: true, force: true });
268
+ if (ownTmp) (0, import_node_fs2.rmSync)(tmp, { recursive: true, force: true });
249
269
  }
250
270
  const elapsedMs = Math.round(performance.now() - start);
251
- if (code !== 0 || !(0, import_node_fs.existsSync)(io.outPath)) {
271
+ if (cancelled) {
272
+ return { status: "cancelled", runtimeRequested, elapsedMs };
273
+ }
274
+ if (code !== 0 || !(0, import_node_fs2.existsSync)(io.outPath)) {
252
275
  const failure = lastJsonLine(stderr);
253
276
  const stage = toStage(failure?.stage);
254
277
  const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
@@ -258,7 +281,7 @@ async function runFit(spec, resolved, io) {
258
281
  }
259
282
 
260
283
  // src/predict.ts
261
- var import_node_fs2 = require("fs");
284
+ var import_node_fs3 = require("fs");
262
285
  var import_node_path4 = require("path");
263
286
  var import_core2 = require("@mcmcjs/core");
264
287
  function predictData(spec) {
@@ -279,7 +302,7 @@ async function runPredict(spec, resolved, io) {
279
302
  const ownTmp = io.tmpDir === void 0;
280
303
  const tmp = io.tmpDir ?? makeRequestDir("predict");
281
304
  const requestPath = (0, import_node_path4.join)(tmp, "request.json");
282
- (0, import_node_fs2.writeFileSync)(
305
+ (0, import_node_fs3.writeFileSync)(
283
306
  requestPath,
284
307
  JSON.stringify({
285
308
  mode: "predict",
@@ -306,16 +329,16 @@ async function runPredict(spec, resolved, io) {
306
329
  try {
307
330
  ({ stdout, stderr, code } = await io.spawn(resolved.command, args));
308
331
  } finally {
309
- if (ownTmp) (0, import_node_fs2.rmSync)(tmp, { recursive: true, force: true });
332
+ if (ownTmp) (0, import_node_fs3.rmSync)(tmp, { recursive: true, force: true });
310
333
  }
311
334
  const elapsedMs = Math.round(performance.now() - start);
312
- if (code !== 0 || !(0, import_node_fs2.existsSync)(io.outPath)) {
335
+ if (code !== 0 || !(0, import_node_fs3.existsSync)(io.outPath)) {
313
336
  const failure = lastJsonLine(stderr);
314
337
  const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
315
338
  return { status: "error", runtimeRequested, elapsedMs, stage: toStage(failure?.stage), error };
316
339
  }
317
340
  try {
318
- (0, import_core2.parseSamples)((0, import_node_fs2.readFileSync)(io.outPath, "utf8"));
341
+ (0, import_core2.parseSamples)((0, import_node_fs3.readFileSync)(io.outPath, "utf8"));
319
342
  } catch (error) {
320
343
  return {
321
344
  status: "error",
@@ -335,15 +358,15 @@ async function runPredict(spec, resolved, io) {
335
358
  runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
336
359
  manifest_sha256: provenance?.manifest_sha256,
337
360
  packages: provenance?.packages,
338
- model_sha256: (0, import_node_fs2.existsSync)(spec.modelPath) ? sha256((0, import_node_fs2.readFileSync)(spec.modelPath, "utf8")) : void 0,
361
+ model_sha256: (0, import_node_fs3.existsSync)(spec.modelPath) ? sha256((0, import_node_fs3.readFileSync)(spec.modelPath, "utf8")) : void 0,
339
362
  data_sha256: sha256((0, import_core2.canonicalJson)(data)),
340
363
  posterior_samples: io.samplesPath,
341
- posterior_samples_sha256: (0, import_node_fs2.existsSync)(io.samplesPath) ? sha256((0, import_node_fs2.readFileSync)(io.samplesPath, "utf8")) : void 0,
364
+ posterior_samples_sha256: (0, import_node_fs3.existsSync)(io.samplesPath) ? sha256((0, import_node_fs3.readFileSync)(io.samplesPath, "utf8")) : void 0,
342
365
  samples_file: io.outPath,
343
366
  started_at: startedAt,
344
367
  elapsed_ms: elapsedMs
345
368
  };
346
- (0, import_node_fs2.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
369
+ (0, import_node_fs3.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
347
370
  `);
348
371
  return {
349
372
  status: "ok",
@@ -355,7 +378,7 @@ async function runPredict(spec, resolved, io) {
355
378
  }
356
379
 
357
380
  // src/project.ts
358
- var import_node_fs3 = require("fs");
381
+ var import_node_fs4 = require("fs");
359
382
  var import_node_os3 = require("os");
360
383
  var import_node_path5 = require("path");
361
384
  var import_core3 = require("@mcmcjs/core");
@@ -403,18 +426,23 @@ function managedProjectDir(version, pins) {
403
426
  function sentinelPath(dir) {
404
427
  return (0, import_node_path5.join)(dir, ".mcmcjs-packages.json");
405
428
  }
429
+ var PROVISION_GENERATION = 1;
406
430
  function expectedSentinel(pins) {
407
- return (0, import_core3.canonicalJson)({ packages: [...PACKAGES].sort(), pins: pins ?? {} });
431
+ return (0, import_core3.canonicalJson)({
432
+ gen: PROVISION_GENERATION,
433
+ packages: [...PACKAGES].sort(),
434
+ pins: pins ?? {}
435
+ });
408
436
  }
409
437
  function provisioned(dir, pins) {
410
438
  try {
411
- return (0, import_node_fs3.readFileSync)(sentinelPath(dir), "utf8").trim() === expectedSentinel(pins);
439
+ return (0, import_node_fs4.readFileSync)(sentinelPath(dir), "utf8").trim() === expectedSentinel(pins);
412
440
  } catch {
413
441
  return false;
414
442
  }
415
443
  }
416
444
  function managedProjectReady(dir = managedProjectDir(), pins) {
417
- return (0, import_node_fs3.existsSync)((0, import_node_path5.join)(dir, "Project.toml")) && provisioned(dir, pins);
445
+ return (0, import_node_fs4.existsSync)((0, import_node_path5.join)(dir, "Project.toml")) && provisioned(dir, pins);
418
446
  }
419
447
  function packageSpecsCode(pins) {
420
448
  const specs = PACKAGES.map((name) => {
@@ -423,13 +451,27 @@ function packageSpecsCode(pins) {
423
451
  });
424
452
  return `[${specs.join(", ")}]`;
425
453
  }
426
- async function ensureProject(juliaBin, run = (0, import_engine3.createRunner)(30 * 6e4), dir = managedProjectDir(), pins) {
454
+ function usesPinnedEnv(version, pins) {
455
+ return !hasPins(pins) && version === import_core3.DEFAULT_JULIA_CHANNEL && (0, import_node_fs4.existsSync)(pinnedEnvDir());
456
+ }
457
+ async function ensureProject(juliaBin, run = (0, import_engine3.createRunner)(30 * 6e4), dir = managedProjectDir(), pins, opts) {
427
458
  validatePins(pins);
428
459
  if (managedProjectReady(dir, pins)) return dir;
429
- (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
430
- const code = `using Pkg; Pkg.add(${packageSpecsCode(pins)}); Pkg.precompile()`;
431
- await run(juliaBin, ["--startup-file=no", `--project=${dir}`, "-e", code]);
432
- (0, import_node_fs3.writeFileSync)(sentinelPath(dir), expectedSentinel(pins));
460
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
461
+ if (usesPinnedEnv(opts?.version, pins)) {
462
+ (0, import_node_fs4.copyFileSync)((0, import_node_path5.join)(pinnedEnvDir(), "Project.toml"), (0, import_node_path5.join)(dir, "Project.toml"));
463
+ (0, import_node_fs4.copyFileSync)((0, import_node_path5.join)(pinnedEnvDir(), "Manifest.toml"), (0, import_node_path5.join)(dir, "Manifest.toml"));
464
+ await run(juliaBin, [
465
+ "--startup-file=no",
466
+ `--project=${dir}`,
467
+ "-e",
468
+ "using Pkg; Pkg.instantiate(); Pkg.precompile()"
469
+ ]);
470
+ } else {
471
+ const code = `using Pkg; Pkg.add(${packageSpecsCode(pins)}); Pkg.precompile()`;
472
+ await run(juliaBin, ["--startup-file=no", `--project=${dir}`, "-e", code]);
473
+ }
474
+ (0, import_node_fs4.writeFileSync)(sentinelPath(dir), expectedSentinel(pins));
433
475
  return dir;
434
476
  }
435
477
 
@@ -463,6 +505,7 @@ async function runMatrix(spec, versions, io) {
463
505
  entry = { version, status: "error", elapsedMs: 0, error: error.message };
464
506
  }
465
507
  entries.push(entry);
508
+ if (entry.status === "cancelled") break;
466
509
  if (entry.status === "error" && !io.keepGoing) break;
467
510
  }
468
511
  return {
@@ -472,28 +515,38 @@ async function runMatrix(spec, versions, io) {
472
515
  }
473
516
 
474
517
  // src/setup.ts
518
+ var import_core4 = require("@mcmcjs/core");
475
519
  var JULIAUP_INSTALL_URL = "https://install.julialang.org";
476
520
  function juliaupInstallCommand(platform) {
477
521
  if (platform === "win32") {
478
522
  return null;
479
523
  }
480
- return { command: "sh", args: ["-c", `curl -fsSL ${JULIAUP_INSTALL_URL} | sh -s -- --yes`] };
524
+ return {
525
+ command: "sh",
526
+ args: [
527
+ "-c",
528
+ `curl -fsSL ${JULIAUP_INSTALL_URL} | sh -s -- --yes --default-channel ${import_core4.DEFAULT_JULIA_CHANNEL}`
529
+ ]
530
+ };
481
531
  }
482
532
  function planSetup(report, platform) {
483
- if (report.julia.found) return [];
533
+ if (report.julia.found && report.julia.version === import_core4.DEFAULT_JULIA_CHANNEL) return [];
484
534
  if (report.juliaup.found) {
485
535
  return [
486
536
  {
487
537
  tool: "julia",
488
- label: "install the latest stable Julia via juliaup",
489
- command: { command: report.juliaup.path ?? "juliaup", args: ["add", "release"] }
538
+ label: `install the pinned Julia ${import_core4.DEFAULT_JULIA_CHANNEL} via juliaup`,
539
+ command: {
540
+ command: report.juliaup.path ?? "juliaup",
541
+ args: ["add", import_core4.DEFAULT_JULIA_CHANNEL]
542
+ }
490
543
  }
491
544
  ];
492
545
  }
493
546
  return [
494
547
  {
495
548
  tool: "juliaup",
496
- label: "install juliaup (this also installs Julia)",
549
+ label: `install juliaup with the pinned Julia ${import_core4.DEFAULT_JULIA_CHANNEL}`,
497
550
  command: juliaupInstallCommand(platform)
498
551
  }
499
552
  ];
@@ -506,8 +559,8 @@ async function runSetup(options = {}) {
506
559
  dryRun = false
507
560
  } = options;
508
561
  const before = await runDoctor(runner);
509
- if (before.ready) return { ...before, steps: [] };
510
562
  const plan = planSetup(before, platform);
563
+ if (plan.length === 0) return { ...before, steps: [] };
511
564
  if (dryRun) {
512
565
  return {
513
566
  ...before,
@@ -612,7 +665,7 @@ function assertVersionsInstalled(channels, installed) {
612
665
 
613
666
  // src/worker.ts
614
667
  var import_node_child_process = require("child_process");
615
- var import_node_fs4 = require("fs");
668
+ var import_node_fs5 = require("fs");
616
669
  var import_node_net = require("net");
617
670
  var import_node_path7 = require("path");
618
671
  var import_engine4 = require("@mcmcjs/engine");
@@ -625,10 +678,31 @@ function fitInactivityTimeoutMs() {
625
678
  }
626
679
  var WorkerTimeoutError = class extends Error {
627
680
  };
681
+ var FitCancelledError = class extends Error {
682
+ };
683
+ function workerPidPath(socket) {
684
+ return `${socket}.pid`;
685
+ }
686
+ function killWorker(socket) {
687
+ const pidPath = workerPidPath(socket);
688
+ try {
689
+ const pid = Number((0, import_node_fs5.readFileSync)(pidPath, "utf8").trim());
690
+ if (Number.isInteger(pid) && pid > 0) {
691
+ try {
692
+ process.kill(-pid, "SIGKILL");
693
+ } catch {
694
+ process.kill(pid, "SIGKILL");
695
+ }
696
+ }
697
+ } catch {
698
+ }
699
+ (0, import_node_fs5.rmSync)(socket, { force: true });
700
+ (0, import_node_fs5.rmSync)(pidPath, { force: true });
701
+ }
628
702
  function workersDir() {
629
703
  const runtime = process.env.XDG_RUNTIME_DIR;
630
704
  const dir = runtime ? (0, import_node_path7.join)(runtime, "mcmcjs") : (0, import_node_path7.join)(sharedTmpParent(), "workers");
631
- (0, import_node_fs4.mkdirSync)(dir, { recursive: true, mode: 448 });
705
+ (0, import_node_fs5.mkdirSync)(dir, { recursive: true, mode: 448 });
632
706
  return dir;
633
707
  }
634
708
  function workerSocketPath(command, projectDir) {
@@ -656,13 +730,13 @@ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
656
730
  function acquireStartLock(socketPath) {
657
731
  const lock = `${socketPath}.start`;
658
732
  try {
659
- (0, import_node_fs4.mkdirSync)(lock);
733
+ (0, import_node_fs5.mkdirSync)(lock);
660
734
  return true;
661
735
  } catch {
662
736
  try {
663
- if (Date.now() - (0, import_node_fs4.statSync)(lock).mtimeMs > START_WAIT_MS) {
664
- (0, import_node_fs4.rmdirSync)(lock);
665
- (0, import_node_fs4.mkdirSync)(lock);
737
+ if (Date.now() - (0, import_node_fs5.statSync)(lock).mtimeMs > START_WAIT_MS) {
738
+ (0, import_node_fs5.rmdirSync)(lock);
739
+ (0, import_node_fs5.mkdirSync)(lock);
666
740
  return true;
667
741
  }
668
742
  } catch {
@@ -672,21 +746,22 @@ function acquireStartLock(socketPath) {
672
746
  }
673
747
  function releaseStartLock(socketPath) {
674
748
  try {
675
- (0, import_node_fs4.rmdirSync)(`${socketPath}.start`);
749
+ (0, import_node_fs5.rmdirSync)(`${socketPath}.start`);
676
750
  } catch {
677
751
  }
678
752
  }
679
- async function ensureWorker(resolved, projectDir, notify) {
753
+ async function ensureWorker(resolved, projectDir, notify, signal) {
680
754
  const path = workerSocketPath(resolved.command, projectDir);
681
755
  try {
682
756
  return await connectOnce(path);
683
757
  } catch {
684
758
  }
759
+ if (signal?.aborted) throw new FitCancelledError();
685
760
  let spawnFailure;
686
761
  const startedHere = acquireStartLock(path);
687
762
  if (startedHere) {
688
763
  try {
689
- (0, import_node_fs4.rmSync)(path, { force: true });
764
+ (0, import_node_fs5.rmSync)(path, { force: true });
690
765
  notify?.("starting the Julia worker (first use loads Turing, can take a minute)...");
691
766
  const child = (0, import_node_child_process.spawn)(
692
767
  resolved.command,
@@ -710,6 +785,7 @@ async function ensureWorker(resolved, projectDir, notify) {
710
785
  try {
711
786
  const deadline = Date.now() + START_WAIT_MS;
712
787
  for (; ; ) {
788
+ if (signal?.aborted) throw new FitCancelledError();
713
789
  if (spawnFailure) throw spawnFailure;
714
790
  try {
715
791
  return await connectOnce(path);
@@ -722,16 +798,27 @@ async function ensureWorker(resolved, projectDir, notify) {
722
798
  if (startedHere) releaseStartLock(path);
723
799
  }
724
800
  }
725
- function readWorkerResponse(sock, onProgress, inactivityMs = fitInactivityTimeoutMs()) {
801
+ function readWorkerResponse(sock, onProgress, onDraws, signal, inactivityMs = fitInactivityTimeoutMs()) {
726
802
  return new Promise((resolve, reject) => {
727
803
  let pending = "";
728
804
  let settled = false;
805
+ const onAbort = () => {
806
+ settle(() => {
807
+ sock.destroy();
808
+ reject(new FitCancelledError());
809
+ });
810
+ };
729
811
  const settle = (fn) => {
730
812
  if (settled) return;
731
813
  settled = true;
732
814
  sock.setTimeout(0);
815
+ signal?.removeEventListener("abort", onAbort);
733
816
  fn();
734
817
  };
818
+ if (signal) {
819
+ if (signal.aborted) onAbort();
820
+ else signal.addEventListener("abort", onAbort, { once: true });
821
+ }
735
822
  sock.setTimeout(inactivityMs, () => {
736
823
  const error = new WorkerTimeoutError(
737
824
  `the worker sent nothing for ${Math.round(inactivityMs / 6e4)} minutes`
@@ -754,6 +841,11 @@ function readWorkerResponse(sock, onProgress, inactivityMs = fitInactivityTimeou
754
841
  onProgress?.(progress);
755
842
  continue;
756
843
  }
844
+ const batch = (0, import_engine4.parseDrawBatchLine)(line);
845
+ if (batch) {
846
+ onDraws?.(batch);
847
+ continue;
848
+ }
757
849
  try {
758
850
  const doc = JSON.parse(line);
759
851
  settle(() => {
@@ -772,10 +864,48 @@ function readWorkerResponse(sock, onProgress, inactivityMs = fitInactivityTimeou
772
864
  async function runFitViaWorker(spec, resolved, io) {
773
865
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
774
866
  const start = performance.now();
775
- const sock = await ensureWorker(resolved, io.projectDir, io.notify);
776
- sock.write(`${JSON.stringify(fitRequest(spec, io.outPath))}
777
- `);
778
- const final = await readWorkerResponse(sock, io.onProgress);
867
+ if (io.signal?.aborted) {
868
+ return { status: "cancelled", runtimeRequested: spec.backend.version, elapsedMs: 0 };
869
+ }
870
+ let sock;
871
+ try {
872
+ sock = await ensureWorker(resolved, io.projectDir, io.notify, io.signal);
873
+ } catch (error) {
874
+ if (error instanceof FitCancelledError) {
875
+ return {
876
+ status: "cancelled",
877
+ runtimeRequested: spec.backend.version,
878
+ elapsedMs: Math.round(performance.now() - start)
879
+ };
880
+ }
881
+ throw error;
882
+ }
883
+ if (io.signal?.aborted) {
884
+ sock.destroy();
885
+ return {
886
+ status: "cancelled",
887
+ runtimeRequested: spec.backend.version,
888
+ elapsedMs: Math.round(performance.now() - start)
889
+ };
890
+ }
891
+ sock.write(
892
+ `${JSON.stringify(fitRequest(spec, io.outPath, { streamDraws: io.onDraws !== void 0 }))}
893
+ `
894
+ );
895
+ let final;
896
+ try {
897
+ final = await readWorkerResponse(sock, io.onProgress, io.onDraws, io.signal);
898
+ } catch (error) {
899
+ if (error instanceof FitCancelledError) {
900
+ killWorker(workerSocketPath(resolved.command, io.projectDir));
901
+ return {
902
+ status: "cancelled",
903
+ runtimeRequested: spec.backend.version,
904
+ elapsedMs: Math.round(performance.now() - start)
905
+ };
906
+ }
907
+ throw error;
908
+ }
779
909
  const elapsedMs = Math.round(performance.now() - start);
780
910
  if (final.ok !== true) {
781
911
  if (final.stage === "worker") throw new Error(String(final.error ?? "worker error"));
@@ -797,6 +927,9 @@ async function runFitViaWorker(spec, resolved, io) {
797
927
  );
798
928
  }
799
929
  async function runFitAuto(spec, resolved, io) {
930
+ if (io.signal?.aborted) {
931
+ return { status: "cancelled", runtimeRequested: spec.backend.version, elapsedMs: 0 };
932
+ }
800
933
  if (io.daemon && process.platform !== "win32") {
801
934
  const start = performance.now();
802
935
  try {
@@ -820,7 +953,7 @@ async function runFitAuto(spec, resolved, io) {
820
953
  }
821
954
  async function listWorkers() {
822
955
  const dir = workersDir();
823
- const sockets = (0, import_node_fs4.readdirSync)(dir).filter((f) => f.startsWith("worker-") && f.endsWith(".sock"));
956
+ const sockets = (0, import_node_fs5.readdirSync)(dir).filter((f) => f.startsWith("worker-") && f.endsWith(".sock"));
824
957
  const statuses = [];
825
958
  for (const name of sockets) {
826
959
  const socket = (0, import_node_path7.join)(dir, name);
@@ -840,7 +973,7 @@ async function stopWorker(socket) {
840
973
  try {
841
974
  sock = await connectOnce(socket);
842
975
  } catch {
843
- (0, import_node_fs4.rmSync)(socket, { force: true });
976
+ (0, import_node_fs5.rmSync)(socket, { force: true });
844
977
  return "stale";
845
978
  }
846
979
  const acked = await new Promise((resolve) => {
@@ -856,7 +989,7 @@ async function stopWorker(socket) {
856
989
  });
857
990
  sock.destroy();
858
991
  if (!acked) return "pending";
859
- (0, import_node_fs4.rmSync)(socket, { force: true });
992
+ (0, import_node_fs5.rmSync)(socket, { force: true });
860
993
  return "stopped";
861
994
  }
862
995
  // Annotate the CommonJS export names for ESM import in node:
@@ -869,6 +1002,7 @@ async function stopWorker(socket) {
869
1002
  gcVersions,
870
1003
  juliaEngine,
871
1004
  juliaupInstallCommand,
1005
+ killWorker,
872
1006
  listVersions,
873
1007
  listWorkers,
874
1008
  managedProjectDir,