@mcmcjs/julia 0.6.1 → 0.7.1

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.js CHANGED
@@ -53,7 +53,7 @@ var juliaEngine = {
53
53
  };
54
54
 
55
55
  // src/fit.ts
56
- import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
56
+ import { existsSync as existsSync2, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
57
57
  import { join as join3 } from "path";
58
58
  import {
59
59
  canonicalJson,
@@ -63,6 +63,7 @@ import {
63
63
 
64
64
  // src/runner-common.ts
65
65
  import { createHash } from "crypto";
66
+ import { existsSync } from "fs";
66
67
  import { tmpdir } from "os";
67
68
  import { dirname, join as join2 } from "path";
68
69
  import { fileURLToPath } from "url";
@@ -70,11 +71,19 @@ function sharedTmpParent() {
70
71
  const uid = typeof process.getuid === "function" ? `-${process.getuid()}` : "";
71
72
  return join2(tmpdir(), `mcmcjs${uid}`);
72
73
  }
74
+ function driverFile(name) {
75
+ const here = dirname(fileURLToPath(import.meta.url));
76
+ const beside = join2(here, name);
77
+ return existsSync(beside) ? beside : join2(here, "driver", name);
78
+ }
73
79
  function driverPath() {
74
- return join2(dirname(fileURLToPath(import.meta.url)), "driver.jl");
80
+ return driverFile("driver.jl");
75
81
  }
76
82
  function workerPath() {
77
- return join2(dirname(fileURLToPath(import.meta.url)), "worker.jl");
83
+ return driverFile("worker.jl");
84
+ }
85
+ function pinnedEnvDir() {
86
+ return join2(dirname(fileURLToPath(import.meta.url)), "julia-env");
78
87
  }
79
88
  function sha256(text) {
80
89
  return createHash("sha256").update(text).digest("hex");
@@ -110,7 +119,7 @@ function makeRequestDir(kind) {
110
119
  mkdirSync(parent, { recursive: true });
111
120
  return mkdtempSync(join3(parent, `${kind}-`));
112
121
  }
113
- function fitRequest(spec, outPath) {
122
+ function fitRequest(spec, outPath, opts) {
114
123
  return {
115
124
  schema_version: spec.schema_version,
116
125
  backend: { id: spec.backend.id },
@@ -118,12 +127,13 @@ function fitRequest(spec, outPath) {
118
127
  data: spec.data,
119
128
  sampler: spec.sampler,
120
129
  seed: spec.seed,
121
- out: outPath
130
+ out: outPath,
131
+ ...opts?.streamDraws ? { stream_draws: true } : {}
122
132
  };
123
133
  }
124
134
  function finalizeOkFit(spec, resolved, io, provenance, startedAt, elapsedMs) {
125
135
  const runtimeRequested = spec.backend.version;
126
- if (!existsSync(io.outPath)) {
136
+ if (!existsSync2(io.outPath)) {
127
137
  return {
128
138
  status: "error",
129
139
  runtimeRequested,
@@ -152,7 +162,7 @@ function finalizeOkFit(spec, resolved, io, provenance, startedAt, elapsedMs) {
152
162
  runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
153
163
  manifest_sha256: provenance?.manifest_sha256,
154
164
  packages: provenance?.packages,
155
- model_sha256: existsSync(spec.modelPath) ? sha256(readFileSync(spec.modelPath, "utf8")) : void 0,
165
+ model_sha256: existsSync2(spec.modelPath) ? sha256(readFileSync(spec.modelPath, "utf8")) : void 0,
156
166
  data_sha256: io.dataSha256 ?? sha256(canonicalJson(spec.data)),
157
167
  ...io.dataFile ? { data_file: io.dataFile } : {},
158
168
  samples_file: io.outPath,
@@ -173,10 +183,16 @@ async function runFit(spec, resolved, io) {
173
183
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
174
184
  const start = performance.now();
175
185
  const runtimeRequested = spec.backend.version;
186
+ if (io.signal?.aborted) {
187
+ return { status: "cancelled", runtimeRequested, elapsedMs: 0 };
188
+ }
176
189
  const ownTmp = io.tmpDir === void 0;
177
190
  const tmp = io.tmpDir ?? makeRequestDir("fit");
178
191
  const requestPath = join3(tmp, "request.json");
179
- writeFileSync(requestPath, JSON.stringify(fitRequest(spec, io.outPath)));
192
+ writeFileSync(
193
+ requestPath,
194
+ JSON.stringify(fitRequest(spec, io.outPath, { streamDraws: io.onDraws !== void 0 }))
195
+ );
180
196
  const args = [
181
197
  ...resolved.args,
182
198
  "--startup-file=no",
@@ -187,15 +203,21 @@ async function runFit(spec, resolved, io) {
187
203
  let stdout;
188
204
  let stderr;
189
205
  let code;
206
+ let cancelled;
190
207
  try {
191
- ({ stdout, stderr, code } = await io.spawn(resolved.command, args, {
192
- onProgress: io.onProgress
208
+ ({ stdout, stderr, code, cancelled } = await io.spawn(resolved.command, args, {
209
+ onProgress: io.onProgress,
210
+ onDraws: io.onDraws,
211
+ signal: io.signal
193
212
  }));
194
213
  } finally {
195
214
  if (ownTmp) rmSync(tmp, { recursive: true, force: true });
196
215
  }
197
216
  const elapsedMs = Math.round(performance.now() - start);
198
- if (code !== 0 || !existsSync(io.outPath)) {
217
+ if (cancelled) {
218
+ return { status: "cancelled", runtimeRequested, elapsedMs };
219
+ }
220
+ if (code !== 0 || !existsSync2(io.outPath)) {
199
221
  const failure = lastJsonLine(stderr);
200
222
  const stage = toStage(failure?.stage);
201
223
  const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
@@ -205,7 +227,7 @@ async function runFit(spec, resolved, io) {
205
227
  }
206
228
 
207
229
  // src/predict.ts
208
- import { existsSync as existsSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
230
+ import { existsSync as existsSync3, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
209
231
  import { join as join4 } from "path";
210
232
  import {
211
233
  canonicalJson as canonicalJson2,
@@ -260,7 +282,7 @@ async function runPredict(spec, resolved, io) {
260
282
  if (ownTmp) rmSync2(tmp, { recursive: true, force: true });
261
283
  }
262
284
  const elapsedMs = Math.round(performance.now() - start);
263
- if (code !== 0 || !existsSync2(io.outPath)) {
285
+ if (code !== 0 || !existsSync3(io.outPath)) {
264
286
  const failure = lastJsonLine(stderr);
265
287
  const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
266
288
  return { status: "error", runtimeRequested, elapsedMs, stage: toStage(failure?.stage), error };
@@ -286,10 +308,10 @@ async function runPredict(spec, resolved, io) {
286
308
  runtime: { requested: runtimeRequested, actual: julia, path: resolved.command },
287
309
  manifest_sha256: provenance?.manifest_sha256,
288
310
  packages: provenance?.packages,
289
- model_sha256: existsSync2(spec.modelPath) ? sha256(readFileSync2(spec.modelPath, "utf8")) : void 0,
311
+ model_sha256: existsSync3(spec.modelPath) ? sha256(readFileSync2(spec.modelPath, "utf8")) : void 0,
290
312
  data_sha256: sha256(canonicalJson2(data)),
291
313
  posterior_samples: io.samplesPath,
292
- posterior_samples_sha256: existsSync2(io.samplesPath) ? sha256(readFileSync2(io.samplesPath, "utf8")) : void 0,
314
+ posterior_samples_sha256: existsSync3(io.samplesPath) ? sha256(readFileSync2(io.samplesPath, "utf8")) : void 0,
293
315
  samples_file: io.outPath,
294
316
  started_at: startedAt,
295
317
  elapsed_ms: elapsedMs
@@ -306,10 +328,10 @@ async function runPredict(spec, resolved, io) {
306
328
  }
307
329
 
308
330
  // src/project.ts
309
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
331
+ import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
310
332
  import { homedir as homedir2 } from "os";
311
333
  import { join as join5 } from "path";
312
- import { canonicalJson as canonicalJson3 } from "@mcmcjs/core";
334
+ import { canonicalJson as canonicalJson3, DEFAULT_JULIA_CHANNEL } from "@mcmcjs/core";
313
335
  import { createRunner as createRunner3 } from "@mcmcjs/engine";
314
336
  var PACKAGES = [
315
337
  "Turing",
@@ -354,8 +376,13 @@ function managedProjectDir(version, pins) {
354
376
  function sentinelPath(dir) {
355
377
  return join5(dir, ".mcmcjs-packages.json");
356
378
  }
379
+ var PROVISION_GENERATION = 1;
357
380
  function expectedSentinel(pins) {
358
- return canonicalJson3({ packages: [...PACKAGES].sort(), pins: pins ?? {} });
381
+ return canonicalJson3({
382
+ gen: PROVISION_GENERATION,
383
+ packages: [...PACKAGES].sort(),
384
+ pins: pins ?? {}
385
+ });
359
386
  }
360
387
  function provisioned(dir, pins) {
361
388
  try {
@@ -365,7 +392,7 @@ function provisioned(dir, pins) {
365
392
  }
366
393
  }
367
394
  function managedProjectReady(dir = managedProjectDir(), pins) {
368
- return existsSync3(join5(dir, "Project.toml")) && provisioned(dir, pins);
395
+ return existsSync4(join5(dir, "Project.toml")) && provisioned(dir, pins);
369
396
  }
370
397
  function packageSpecsCode(pins) {
371
398
  const specs = PACKAGES.map((name) => {
@@ -374,12 +401,26 @@ function packageSpecsCode(pins) {
374
401
  });
375
402
  return `[${specs.join(", ")}]`;
376
403
  }
377
- async function ensureProject(juliaBin, run = createRunner3(30 * 6e4), dir = managedProjectDir(), pins) {
404
+ function usesPinnedEnv(version, pins) {
405
+ return !hasPins(pins) && version === DEFAULT_JULIA_CHANNEL && existsSync4(pinnedEnvDir());
406
+ }
407
+ async function ensureProject(juliaBin, run = createRunner3(30 * 6e4), dir = managedProjectDir(), pins, opts) {
378
408
  validatePins(pins);
379
409
  if (managedProjectReady(dir, pins)) return dir;
380
410
  mkdirSync2(dir, { recursive: true });
381
- const code = `using Pkg; Pkg.add(${packageSpecsCode(pins)}); Pkg.precompile()`;
382
- await run(juliaBin, ["--startup-file=no", `--project=${dir}`, "-e", code]);
411
+ if (usesPinnedEnv(opts?.version, pins)) {
412
+ copyFileSync(join5(pinnedEnvDir(), "Project.toml"), join5(dir, "Project.toml"));
413
+ copyFileSync(join5(pinnedEnvDir(), "Manifest.toml"), join5(dir, "Manifest.toml"));
414
+ await run(juliaBin, [
415
+ "--startup-file=no",
416
+ `--project=${dir}`,
417
+ "-e",
418
+ "using Pkg; Pkg.instantiate(); Pkg.precompile()"
419
+ ]);
420
+ } else {
421
+ const code = `using Pkg; Pkg.add(${packageSpecsCode(pins)}); Pkg.precompile()`;
422
+ await run(juliaBin, ["--startup-file=no", `--project=${dir}`, "-e", code]);
423
+ }
383
424
  writeFileSync3(sentinelPath(dir), expectedSentinel(pins));
384
425
  return dir;
385
426
  }
@@ -414,6 +455,7 @@ async function runMatrix(spec, versions, io) {
414
455
  entry = { version, status: "error", elapsedMs: 0, error: error.message };
415
456
  }
416
457
  entries.push(entry);
458
+ if (entry.status === "cancelled") break;
417
459
  if (entry.status === "error" && !io.keepGoing) break;
418
460
  }
419
461
  return {
@@ -423,28 +465,38 @@ async function runMatrix(spec, versions, io) {
423
465
  }
424
466
 
425
467
  // src/setup.ts
468
+ import { DEFAULT_JULIA_CHANNEL as DEFAULT_JULIA_CHANNEL2 } from "@mcmcjs/core";
426
469
  var JULIAUP_INSTALL_URL = "https://install.julialang.org";
427
470
  function juliaupInstallCommand(platform) {
428
471
  if (platform === "win32") {
429
472
  return null;
430
473
  }
431
- return { command: "sh", args: ["-c", `curl -fsSL ${JULIAUP_INSTALL_URL} | sh -s -- --yes`] };
474
+ return {
475
+ command: "sh",
476
+ args: [
477
+ "-c",
478
+ `curl -fsSL ${JULIAUP_INSTALL_URL} | sh -s -- --yes --default-channel ${DEFAULT_JULIA_CHANNEL2}`
479
+ ]
480
+ };
432
481
  }
433
482
  function planSetup(report, platform) {
434
- if (report.julia.found) return [];
483
+ if (report.julia.found && report.julia.version === DEFAULT_JULIA_CHANNEL2) return [];
435
484
  if (report.juliaup.found) {
436
485
  return [
437
486
  {
438
487
  tool: "julia",
439
- label: "install the latest stable Julia via juliaup",
440
- command: { command: report.juliaup.path ?? "juliaup", args: ["add", "release"] }
488
+ label: `install the pinned Julia ${DEFAULT_JULIA_CHANNEL2} via juliaup`,
489
+ command: {
490
+ command: report.juliaup.path ?? "juliaup",
491
+ args: ["add", DEFAULT_JULIA_CHANNEL2]
492
+ }
441
493
  }
442
494
  ];
443
495
  }
444
496
  return [
445
497
  {
446
498
  tool: "juliaup",
447
- label: "install juliaup (this also installs Julia)",
499
+ label: `install juliaup with the pinned Julia ${DEFAULT_JULIA_CHANNEL2}`,
448
500
  command: juliaupInstallCommand(platform)
449
501
  }
450
502
  ];
@@ -457,8 +509,8 @@ async function runSetup(options = {}) {
457
509
  dryRun = false
458
510
  } = options;
459
511
  const before = await runDoctor(runner);
460
- if (before.ready) return { ...before, steps: [] };
461
512
  const plan = planSetup(before, platform);
513
+ if (plan.length === 0) return { ...before, steps: [] };
462
514
  if (dryRun) {
463
515
  return {
464
516
  ...before,
@@ -563,10 +615,10 @@ function assertVersionsInstalled(channels, installed) {
563
615
 
564
616
  // src/worker.ts
565
617
  import { spawn } from "child_process";
566
- import { mkdirSync as mkdirSync3, readdirSync, rmdirSync, rmSync as rmSync3, statSync } from "fs";
618
+ import { mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync4, rmdirSync, rmSync as rmSync3, statSync } from "fs";
567
619
  import { createConnection } from "net";
568
620
  import { join as join7 } from "path";
569
- import { parseProgressLine } from "@mcmcjs/engine";
621
+ import { parseDrawBatchLine, parseProgressLine } from "@mcmcjs/engine";
570
622
  var START_WAIT_MS = 18e4;
571
623
  var CONNECT_TIMEOUT_MS = 1e3;
572
624
  var STOP_ACK_TIMEOUT_MS = 3e3;
@@ -576,6 +628,27 @@ function fitInactivityTimeoutMs() {
576
628
  }
577
629
  var WorkerTimeoutError = class extends Error {
578
630
  };
631
+ var FitCancelledError = class extends Error {
632
+ };
633
+ function workerPidPath(socket) {
634
+ return `${socket}.pid`;
635
+ }
636
+ function killWorker(socket) {
637
+ const pidPath = workerPidPath(socket);
638
+ try {
639
+ const pid = Number(readFileSync4(pidPath, "utf8").trim());
640
+ if (Number.isInteger(pid) && pid > 0) {
641
+ try {
642
+ process.kill(-pid, "SIGKILL");
643
+ } catch {
644
+ process.kill(pid, "SIGKILL");
645
+ }
646
+ }
647
+ } catch {
648
+ }
649
+ rmSync3(socket, { force: true });
650
+ rmSync3(pidPath, { force: true });
651
+ }
579
652
  function workersDir() {
580
653
  const runtime = process.env.XDG_RUNTIME_DIR;
581
654
  const dir = runtime ? join7(runtime, "mcmcjs") : join7(sharedTmpParent(), "workers");
@@ -627,12 +700,13 @@ function releaseStartLock(socketPath) {
627
700
  } catch {
628
701
  }
629
702
  }
630
- async function ensureWorker(resolved, projectDir, notify) {
703
+ async function ensureWorker(resolved, projectDir, notify, signal) {
631
704
  const path = workerSocketPath(resolved.command, projectDir);
632
705
  try {
633
706
  return await connectOnce(path);
634
707
  } catch {
635
708
  }
709
+ if (signal?.aborted) throw new FitCancelledError();
636
710
  let spawnFailure;
637
711
  const startedHere = acquireStartLock(path);
638
712
  if (startedHere) {
@@ -661,6 +735,7 @@ async function ensureWorker(resolved, projectDir, notify) {
661
735
  try {
662
736
  const deadline = Date.now() + START_WAIT_MS;
663
737
  for (; ; ) {
738
+ if (signal?.aborted) throw new FitCancelledError();
664
739
  if (spawnFailure) throw spawnFailure;
665
740
  try {
666
741
  return await connectOnce(path);
@@ -673,16 +748,27 @@ async function ensureWorker(resolved, projectDir, notify) {
673
748
  if (startedHere) releaseStartLock(path);
674
749
  }
675
750
  }
676
- function readWorkerResponse(sock, onProgress, inactivityMs = fitInactivityTimeoutMs()) {
751
+ function readWorkerResponse(sock, onProgress, onDraws, signal, inactivityMs = fitInactivityTimeoutMs()) {
677
752
  return new Promise((resolve, reject) => {
678
753
  let pending = "";
679
754
  let settled = false;
755
+ const onAbort = () => {
756
+ settle(() => {
757
+ sock.destroy();
758
+ reject(new FitCancelledError());
759
+ });
760
+ };
680
761
  const settle = (fn) => {
681
762
  if (settled) return;
682
763
  settled = true;
683
764
  sock.setTimeout(0);
765
+ signal?.removeEventListener("abort", onAbort);
684
766
  fn();
685
767
  };
768
+ if (signal) {
769
+ if (signal.aborted) onAbort();
770
+ else signal.addEventListener("abort", onAbort, { once: true });
771
+ }
686
772
  sock.setTimeout(inactivityMs, () => {
687
773
  const error = new WorkerTimeoutError(
688
774
  `the worker sent nothing for ${Math.round(inactivityMs / 6e4)} minutes`
@@ -705,6 +791,11 @@ function readWorkerResponse(sock, onProgress, inactivityMs = fitInactivityTimeou
705
791
  onProgress?.(progress);
706
792
  continue;
707
793
  }
794
+ const batch = parseDrawBatchLine(line);
795
+ if (batch) {
796
+ onDraws?.(batch);
797
+ continue;
798
+ }
708
799
  try {
709
800
  const doc = JSON.parse(line);
710
801
  settle(() => {
@@ -723,10 +814,48 @@ function readWorkerResponse(sock, onProgress, inactivityMs = fitInactivityTimeou
723
814
  async function runFitViaWorker(spec, resolved, io) {
724
815
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
725
816
  const start = performance.now();
726
- const sock = await ensureWorker(resolved, io.projectDir, io.notify);
727
- sock.write(`${JSON.stringify(fitRequest(spec, io.outPath))}
728
- `);
729
- const final = await readWorkerResponse(sock, io.onProgress);
817
+ if (io.signal?.aborted) {
818
+ return { status: "cancelled", runtimeRequested: spec.backend.version, elapsedMs: 0 };
819
+ }
820
+ let sock;
821
+ try {
822
+ sock = await ensureWorker(resolved, io.projectDir, io.notify, io.signal);
823
+ } catch (error) {
824
+ if (error instanceof FitCancelledError) {
825
+ return {
826
+ status: "cancelled",
827
+ runtimeRequested: spec.backend.version,
828
+ elapsedMs: Math.round(performance.now() - start)
829
+ };
830
+ }
831
+ throw error;
832
+ }
833
+ if (io.signal?.aborted) {
834
+ sock.destroy();
835
+ return {
836
+ status: "cancelled",
837
+ runtimeRequested: spec.backend.version,
838
+ elapsedMs: Math.round(performance.now() - start)
839
+ };
840
+ }
841
+ sock.write(
842
+ `${JSON.stringify(fitRequest(spec, io.outPath, { streamDraws: io.onDraws !== void 0 }))}
843
+ `
844
+ );
845
+ let final;
846
+ try {
847
+ final = await readWorkerResponse(sock, io.onProgress, io.onDraws, io.signal);
848
+ } catch (error) {
849
+ if (error instanceof FitCancelledError) {
850
+ killWorker(workerSocketPath(resolved.command, io.projectDir));
851
+ return {
852
+ status: "cancelled",
853
+ runtimeRequested: spec.backend.version,
854
+ elapsedMs: Math.round(performance.now() - start)
855
+ };
856
+ }
857
+ throw error;
858
+ }
730
859
  const elapsedMs = Math.round(performance.now() - start);
731
860
  if (final.ok !== true) {
732
861
  if (final.stage === "worker") throw new Error(String(final.error ?? "worker error"));
@@ -748,6 +877,9 @@ async function runFitViaWorker(spec, resolved, io) {
748
877
  );
749
878
  }
750
879
  async function runFitAuto(spec, resolved, io) {
880
+ if (io.signal?.aborted) {
881
+ return { status: "cancelled", runtimeRequested: spec.backend.version, elapsedMs: 0 };
882
+ }
751
883
  if (io.daemon && process.platform !== "win32") {
752
884
  const start = performance.now();
753
885
  try {
@@ -819,6 +951,7 @@ export {
819
951
  gcVersions,
820
952
  juliaEngine,
821
953
  juliaupInstallCommand,
954
+ killWorker,
822
955
  listVersions,
823
956
  listWorkers,
824
957
  managedProjectDir,