@mcmcjs/julia 0.4.0 → 0.5.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
@@ -29,6 +29,7 @@ __export(index_exports, {
29
29
  juliaEngine: () => juliaEngine,
30
30
  juliaupInstallCommand: () => juliaupInstallCommand,
31
31
  listVersions: () => listVersions,
32
+ listWorkers: () => listWorkers,
32
33
  managedProjectDir: () => managedProjectDir,
33
34
  managedProjectReady: () => managedProjectReady,
34
35
  planSetup: () => planSetup,
@@ -37,10 +38,13 @@ __export(index_exports, {
37
38
  resolveVersion: () => resolveVersion,
38
39
  runDoctor: () => runDoctor,
39
40
  runFit: () => runFit,
41
+ runFitAuto: () => runFitAuto,
40
42
  runMatrix: () => runMatrix,
41
43
  runPredict: () => runPredict,
42
44
  runSetup: () => runSetup,
43
45
  setDefaultVersion: () => setDefaultVersion,
46
+ sharedTmpParent: () => sharedTmpParent,
47
+ stopWorker: () => stopWorker,
44
48
  updateVersion: () => updateVersion
45
49
  });
46
50
  module.exports = __toCommonJS(index_exports);
@@ -104,17 +108,24 @@ var juliaEngine = {
104
108
 
105
109
  // src/fit.ts
106
110
  var import_node_fs = require("fs");
107
- var import_node_os2 = require("os");
108
111
  var import_node_path3 = require("path");
109
112
  var import_core = require("@mcmcjs/core");
110
113
 
111
114
  // src/runner-common.ts
112
115
  var import_node_crypto = require("crypto");
116
+ var import_node_os2 = require("os");
113
117
  var import_node_path2 = require("path");
114
118
  var import_node_url = require("url");
119
+ function sharedTmpParent() {
120
+ const uid = typeof process.getuid === "function" ? `-${process.getuid()}` : "";
121
+ return (0, import_node_path2.join)((0, import_node_os2.tmpdir)(), `mcmcjs${uid}`);
122
+ }
115
123
  function driverPath() {
116
124
  return (0, import_node_path2.join)((0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "driver.jl");
117
125
  }
126
+ function workerPath() {
127
+ return (0, import_node_path2.join)((0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(importMetaUrl)), "worker.jl");
128
+ }
118
129
  function sha256(text) {
119
130
  return (0, import_node_crypto.createHash)("sha256").update(text).digest("hex");
120
131
  }
@@ -136,45 +147,40 @@ var STAGES = /* @__PURE__ */ new Set([
136
147
  "load_samples",
137
148
  "predict",
138
149
  "write",
139
- "spawn"
150
+ "spawn",
151
+ "worker"
140
152
  ]);
141
153
  function toStage(value) {
142
154
  return typeof value === "string" && STAGES.has(value) ? value : void 0;
143
155
  }
144
156
 
145
157
  // src/fit.ts
146
- async function runFit(spec, resolved, io) {
147
- const startedAt = (/* @__PURE__ */ new Date()).toISOString();
148
- const start = performance.now();
158
+ function makeRequestDir(kind) {
159
+ const parent = sharedTmpParent();
160
+ (0, import_node_fs.mkdirSync)(parent, { recursive: true });
161
+ return (0, import_node_fs.mkdtempSync)((0, import_node_path3.join)(parent, `${kind}-`));
162
+ }
163
+ function fitRequest(spec, outPath) {
164
+ return {
165
+ schema_version: spec.schema_version,
166
+ backend: { id: spec.backend.id },
167
+ model: { file: spec.modelPath, entry: spec.model.entry },
168
+ data: spec.data,
169
+ sampler: spec.sampler,
170
+ seed: spec.seed,
171
+ out: outPath
172
+ };
173
+ }
174
+ function finalizeOkFit(spec, resolved, io, provenance, startedAt, elapsedMs) {
149
175
  const runtimeRequested = spec.backend.version;
150
- const tmp = io.tmpDir ?? (0, import_node_fs.mkdtempSync)((0, import_node_path3.join)((0, import_node_os2.tmpdir)(), "mcmcjs-fit-"));
151
- const requestPath = (0, import_node_path3.join)(tmp, "request.json");
152
- (0, import_node_fs.writeFileSync)(
153
- requestPath,
154
- JSON.stringify({
155
- schema_version: spec.schema_version,
156
- backend: { id: spec.backend.id },
157
- model: { file: spec.modelPath, entry: spec.model.entry },
158
- data: spec.data,
159
- sampler: spec.sampler,
160
- seed: spec.seed,
161
- out: io.outPath
162
- })
163
- );
164
- const args = [
165
- ...resolved.args,
166
- "--startup-file=no",
167
- `--project=${io.projectDir}`,
168
- driverPath(),
169
- requestPath
170
- ];
171
- const { stdout, stderr, code } = await io.spawn(resolved.command, args);
172
- const elapsedMs = Math.round(performance.now() - start);
173
- if (code !== 0 || !(0, import_node_fs.existsSync)(io.outPath)) {
174
- const failure = lastJsonLine(stderr);
175
- const stage = toStage(failure?.stage);
176
- const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
177
- return { status: "error", runtimeRequested, elapsedMs, stage, error };
176
+ if (!(0, import_node_fs.existsSync)(io.outPath)) {
177
+ return {
178
+ status: "error",
179
+ runtimeRequested,
180
+ elapsedMs,
181
+ stage: "write",
182
+ error: "the runtime reported success but wrote no samples file"
183
+ };
178
184
  }
179
185
  try {
180
186
  (0, import_core.parseSamples)((0, import_node_fs.readFileSync)(io.outPath, "utf8"));
@@ -187,7 +193,6 @@ async function runFit(spec, resolved, io) {
187
193
  error: `samples file did not parse: ${error.message}`
188
194
  };
189
195
  }
190
- const provenance = lastJsonLine(stdout);
191
196
  const julia = provenance?.julia_version;
192
197
  const record = {
193
198
  schema_version: import_core.RUN_RECORD_SCHEMA_VERSION,
@@ -203,7 +208,7 @@ async function runFit(spec, resolved, io) {
203
208
  started_at: startedAt,
204
209
  elapsed_ms: elapsedMs
205
210
  };
206
- (0, import_node_fs.writeFileSync)(`${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
211
+ (0, import_node_fs.writeFileSync)(io.recordPath ?? `${io.outPath}.run.json`, `${JSON.stringify(record, null, 2)}
207
212
  `);
208
213
  return {
209
214
  status: "ok",
@@ -213,10 +218,43 @@ async function runFit(spec, resolved, io) {
213
218
  elapsedMs
214
219
  };
215
220
  }
221
+ async function runFit(spec, resolved, io) {
222
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
223
+ const start = performance.now();
224
+ const runtimeRequested = spec.backend.version;
225
+ const ownTmp = io.tmpDir === void 0;
226
+ const tmp = io.tmpDir ?? makeRequestDir("fit");
227
+ const requestPath = (0, import_node_path3.join)(tmp, "request.json");
228
+ (0, import_node_fs.writeFileSync)(requestPath, JSON.stringify(fitRequest(spec, io.outPath)));
229
+ const args = [
230
+ ...resolved.args,
231
+ "--startup-file=no",
232
+ `--project=${io.projectDir}`,
233
+ driverPath(),
234
+ requestPath
235
+ ];
236
+ let stdout;
237
+ let stderr;
238
+ let code;
239
+ try {
240
+ ({ stdout, stderr, code } = await io.spawn(resolved.command, args, {
241
+ onProgress: io.onProgress
242
+ }));
243
+ } finally {
244
+ if (ownTmp) (0, import_node_fs.rmSync)(tmp, { recursive: true, force: true });
245
+ }
246
+ const elapsedMs = Math.round(performance.now() - start);
247
+ if (code !== 0 || !(0, import_node_fs.existsSync)(io.outPath)) {
248
+ const failure = lastJsonLine(stderr);
249
+ const stage = toStage(failure?.stage);
250
+ const error = failure?.error ?? stderr.trim().split("\n").pop() ?? `julia exited with code ${code}`;
251
+ return { status: "error", runtimeRequested, elapsedMs, stage, error };
252
+ }
253
+ return finalizeOkFit(spec, resolved, io, lastJsonLine(stdout), startedAt, elapsedMs);
254
+ }
216
255
 
217
256
  // src/predict.ts
218
257
  var import_node_fs2 = require("fs");
219
- var import_node_os3 = require("os");
220
258
  var import_node_path4 = require("path");
221
259
  var import_core2 = require("@mcmcjs/core");
222
260
  function predictData(spec) {
@@ -234,7 +272,8 @@ async function runPredict(spec, resolved, io) {
234
272
  const start = performance.now();
235
273
  const runtimeRequested = spec.backend.version;
236
274
  const data = predictData(spec);
237
- const tmp = io.tmpDir ?? (0, import_node_fs2.mkdtempSync)((0, import_node_path4.join)((0, import_node_os3.tmpdir)(), "mcmcjs-predict-"));
275
+ const ownTmp = io.tmpDir === void 0;
276
+ const tmp = io.tmpDir ?? makeRequestDir("predict");
238
277
  const requestPath = (0, import_node_path4.join)(tmp, "request.json");
239
278
  (0, import_node_fs2.writeFileSync)(
240
279
  requestPath,
@@ -257,7 +296,14 @@ async function runPredict(spec, resolved, io) {
257
296
  driverPath(),
258
297
  requestPath
259
298
  ];
260
- const { stdout, stderr, code } = await io.spawn(resolved.command, args);
299
+ let stdout;
300
+ let stderr;
301
+ let code;
302
+ try {
303
+ ({ stdout, stderr, code } = await io.spawn(resolved.command, args));
304
+ } finally {
305
+ if (ownTmp) (0, import_node_fs2.rmSync)(tmp, { recursive: true, force: true });
306
+ }
261
307
  const elapsedMs = Math.round(performance.now() - start);
262
308
  if (code !== 0 || !(0, import_node_fs2.existsSync)(io.outPath)) {
263
309
  const failure = lastJsonLine(stderr);
@@ -306,7 +352,7 @@ async function runPredict(spec, resolved, io) {
306
352
 
307
353
  // src/project.ts
308
354
  var import_node_fs3 = require("fs");
309
- var import_node_os4 = require("os");
355
+ var import_node_os3 = require("os");
310
356
  var import_node_path5 = require("path");
311
357
  var import_engine3 = require("@mcmcjs/engine");
312
358
  var PACKAGES = [
@@ -321,7 +367,7 @@ var PACKAGES = [
321
367
  "StableRNGs"
322
368
  ];
323
369
  function managedProjectDir() {
324
- const base = process.env.XDG_DATA_HOME || (0, import_node_path5.join)((0, import_node_os4.homedir)(), ".local", "share");
370
+ const base = process.env.XDG_DATA_HOME || (0, import_node_path5.join)((0, import_node_os3.homedir)(), ".local", "share");
325
371
  return (0, import_node_path5.join)(base, "mcmcjs", "julia", "env");
326
372
  }
327
373
  function sentinelPath(dir) {
@@ -507,7 +553,7 @@ async function resolveVersion(bin, channel, run) {
507
553
  `Julia version "${channel}" is not installed. Add it with: mcmc julia version add ${channel}`
508
554
  );
509
555
  }
510
- return { command: match.File, args: match.Args ?? [] };
556
+ return { command: match.File, args: match.Args ?? [], version: match.Version };
511
557
  }
512
558
  function assertVersionsInstalled(channels, installed) {
513
559
  const have = new Set(installed.map((v) => v.id));
@@ -519,6 +565,256 @@ function assertVersionsInstalled(channels, installed) {
519
565
  );
520
566
  }
521
567
  }
568
+
569
+ // src/worker.ts
570
+ var import_node_child_process = require("child_process");
571
+ var import_node_fs4 = require("fs");
572
+ var import_node_net = require("net");
573
+ var import_node_path7 = require("path");
574
+ var import_engine4 = require("@mcmcjs/engine");
575
+ var START_WAIT_MS = 18e4;
576
+ var CONNECT_TIMEOUT_MS = 1e3;
577
+ var STOP_ACK_TIMEOUT_MS = 3e3;
578
+ function fitInactivityTimeoutMs() {
579
+ const fromEnv = Number(process.env.MCMC_WORKER_TIMEOUT_MS);
580
+ return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : 30 * 6e4;
581
+ }
582
+ var WorkerTimeoutError = class extends Error {
583
+ };
584
+ function workersDir() {
585
+ const runtime = process.env.XDG_RUNTIME_DIR;
586
+ const dir = runtime ? (0, import_node_path7.join)(runtime, "mcmcjs") : (0, import_node_path7.join)(sharedTmpParent(), "workers");
587
+ (0, import_node_fs4.mkdirSync)(dir, { recursive: true, mode: 448 });
588
+ return dir;
589
+ }
590
+ function workerSocketPath(command, projectDir) {
591
+ return (0, import_node_path7.join)(workersDir(), `worker-${sha256(`${command}
592
+ ${projectDir}`).slice(0, 12)}.sock`);
593
+ }
594
+ function connectOnce(path) {
595
+ return new Promise((resolve, reject) => {
596
+ const sock = (0, import_node_net.createConnection)(path);
597
+ const timer = setTimeout(() => {
598
+ sock.destroy();
599
+ reject(new Error("connect timeout"));
600
+ }, CONNECT_TIMEOUT_MS);
601
+ sock.once("connect", () => {
602
+ clearTimeout(timer);
603
+ resolve(sock);
604
+ });
605
+ sock.once("error", (error) => {
606
+ clearTimeout(timer);
607
+ reject(error);
608
+ });
609
+ });
610
+ }
611
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
612
+ function acquireStartLock(socketPath) {
613
+ const lock = `${socketPath}.start`;
614
+ try {
615
+ (0, import_node_fs4.mkdirSync)(lock);
616
+ return true;
617
+ } catch {
618
+ try {
619
+ if (Date.now() - (0, import_node_fs4.statSync)(lock).mtimeMs > START_WAIT_MS) {
620
+ (0, import_node_fs4.rmdirSync)(lock);
621
+ (0, import_node_fs4.mkdirSync)(lock);
622
+ return true;
623
+ }
624
+ } catch {
625
+ }
626
+ return false;
627
+ }
628
+ }
629
+ function releaseStartLock(socketPath) {
630
+ try {
631
+ (0, import_node_fs4.rmdirSync)(`${socketPath}.start`);
632
+ } catch {
633
+ }
634
+ }
635
+ async function ensureWorker(resolved, projectDir, notify) {
636
+ const path = workerSocketPath(resolved.command, projectDir);
637
+ try {
638
+ return await connectOnce(path);
639
+ } catch {
640
+ }
641
+ let spawnFailure;
642
+ const startedHere = acquireStartLock(path);
643
+ if (startedHere) {
644
+ try {
645
+ (0, import_node_fs4.rmSync)(path, { force: true });
646
+ notify?.("starting the Julia worker (first use loads Turing, can take a minute)...");
647
+ const child = (0, import_node_child_process.spawn)(
648
+ resolved.command,
649
+ [...resolved.args, "--startup-file=no", `--project=${projectDir}`, workerPath(), path],
650
+ { detached: true, stdio: "ignore" }
651
+ );
652
+ child.on("error", (error) => {
653
+ spawnFailure = error;
654
+ });
655
+ child.on("exit", (code) => {
656
+ if (code !== 0 && code !== null) {
657
+ spawnFailure = new Error(`the worker exited with code ${code} before serving`);
658
+ }
659
+ });
660
+ child.unref();
661
+ } catch (error) {
662
+ releaseStartLock(path);
663
+ throw error;
664
+ }
665
+ }
666
+ try {
667
+ const deadline = Date.now() + START_WAIT_MS;
668
+ for (; ; ) {
669
+ if (spawnFailure) throw spawnFailure;
670
+ try {
671
+ return await connectOnce(path);
672
+ } catch {
673
+ }
674
+ if (Date.now() > deadline) throw new Error("the Julia worker did not start in time");
675
+ await delay(250);
676
+ }
677
+ } finally {
678
+ if (startedHere) releaseStartLock(path);
679
+ }
680
+ }
681
+ function readWorkerResponse(sock, onProgress, inactivityMs = fitInactivityTimeoutMs()) {
682
+ return new Promise((resolve, reject) => {
683
+ let pending = "";
684
+ let settled = false;
685
+ const settle = (fn) => {
686
+ if (settled) return;
687
+ settled = true;
688
+ sock.setTimeout(0);
689
+ fn();
690
+ };
691
+ sock.setTimeout(inactivityMs, () => {
692
+ const error = new WorkerTimeoutError(
693
+ `the worker sent nothing for ${Math.round(inactivityMs / 6e4)} minutes`
694
+ );
695
+ settle(() => {
696
+ sock.destroy();
697
+ reject(error);
698
+ });
699
+ });
700
+ sock.on("data", (chunk) => {
701
+ pending += chunk.toString("utf8");
702
+ for (; ; ) {
703
+ const at = pending.indexOf("\n");
704
+ if (at === -1) break;
705
+ const line = pending.slice(0, at).trim();
706
+ pending = pending.slice(at + 1);
707
+ if (!line) continue;
708
+ const progress = (0, import_engine4.parseProgressLine)(line);
709
+ if (progress) {
710
+ onProgress?.(progress);
711
+ continue;
712
+ }
713
+ try {
714
+ const doc = JSON.parse(line);
715
+ settle(() => {
716
+ sock.end();
717
+ resolve(doc);
718
+ });
719
+ return;
720
+ } catch {
721
+ }
722
+ }
723
+ });
724
+ sock.on("error", (error) => settle(() => reject(error)));
725
+ sock.on("close", () => settle(() => reject(new Error("worker closed the connection"))));
726
+ });
727
+ }
728
+ async function runFitViaWorker(spec, resolved, io) {
729
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
730
+ const start = performance.now();
731
+ const sock = await ensureWorker(resolved, io.projectDir, io.notify);
732
+ sock.write(`${JSON.stringify(fitRequest(spec, io.outPath))}
733
+ `);
734
+ const final = await readWorkerResponse(sock, io.onProgress);
735
+ const elapsedMs = Math.round(performance.now() - start);
736
+ if (final.ok !== true) {
737
+ if (final.stage === "worker") throw new Error(String(final.error ?? "worker error"));
738
+ return {
739
+ status: "error",
740
+ runtimeRequested: spec.backend.version,
741
+ elapsedMs,
742
+ stage: toStage(final.stage),
743
+ error: String(final.error ?? "worker error")
744
+ };
745
+ }
746
+ return finalizeOkFit(
747
+ spec,
748
+ resolved,
749
+ io,
750
+ final.provenance,
751
+ startedAt,
752
+ elapsedMs
753
+ );
754
+ }
755
+ async function runFitAuto(spec, resolved, io) {
756
+ if (io.daemon && process.platform !== "win32") {
757
+ const start = performance.now();
758
+ try {
759
+ return await runFitViaWorker(spec, resolved, io);
760
+ } catch (error) {
761
+ if (error instanceof WorkerTimeoutError) {
762
+ return {
763
+ status: "error",
764
+ runtimeRequested: spec.backend.version,
765
+ elapsedMs: Math.round(performance.now() - start),
766
+ stage: "worker",
767
+ error: `${error.message}; the worker may be wedged, try mcmc daemon stop`
768
+ };
769
+ }
770
+ io.notify?.(
771
+ `worker unavailable (${error.message}); falling back to a one-shot run`
772
+ );
773
+ }
774
+ }
775
+ return runFit(spec, resolved, io);
776
+ }
777
+ async function listWorkers() {
778
+ const dir = workersDir();
779
+ const sockets = (0, import_node_fs4.readdirSync)(dir).filter((f) => f.startsWith("worker-") && f.endsWith(".sock"));
780
+ const statuses = [];
781
+ for (const name of sockets) {
782
+ const socket = (0, import_node_path7.join)(dir, name);
783
+ let alive = false;
784
+ try {
785
+ const sock = await connectOnce(socket);
786
+ sock.destroy();
787
+ alive = true;
788
+ } catch {
789
+ }
790
+ statuses.push({ socket, alive });
791
+ }
792
+ return statuses;
793
+ }
794
+ async function stopWorker(socket) {
795
+ let sock;
796
+ try {
797
+ sock = await connectOnce(socket);
798
+ } catch {
799
+ (0, import_node_fs4.rmSync)(socket, { force: true });
800
+ return "stale";
801
+ }
802
+ const acked = await new Promise((resolve) => {
803
+ const timer = setTimeout(() => resolve(false), STOP_ACK_TIMEOUT_MS);
804
+ const finish = (ok) => {
805
+ clearTimeout(timer);
806
+ resolve(ok);
807
+ };
808
+ sock.resume();
809
+ sock.on("close", () => finish(true));
810
+ sock.on("error", () => finish(false));
811
+ sock.write('{"mcmcjs":"stop"}\n');
812
+ });
813
+ sock.destroy();
814
+ if (!acked) return "pending";
815
+ (0, import_node_fs4.rmSync)(socket, { force: true });
816
+ return "stopped";
817
+ }
522
818
  // Annotate the CommonJS export names for ESM import in node:
523
819
  0 && (module.exports = {
524
820
  addVersion,
@@ -530,6 +826,7 @@ function assertVersionsInstalled(channels, installed) {
530
826
  juliaEngine,
531
827
  juliaupInstallCommand,
532
828
  listVersions,
829
+ listWorkers,
533
830
  managedProjectDir,
534
831
  managedProjectReady,
535
832
  planSetup,
@@ -538,10 +835,13 @@ function assertVersionsInstalled(channels, installed) {
538
835
  resolveVersion,
539
836
  runDoctor,
540
837
  runFit,
838
+ runFitAuto,
541
839
  runMatrix,
542
840
  runPredict,
543
841
  runSetup,
544
842
  setDefaultVersion,
843
+ sharedTmpParent,
844
+ stopWorker,
545
845
  updateVersion
546
846
  });
547
847
  //# sourceMappingURL=index.cjs.map