c8ctl-plugin-nano 1.26.2 → 1.26.3

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.
Files changed (2) hide show
  1. package/c8ctl-plugin.js +83 -3
  2. package/package.json +8 -8
package/c8ctl-plugin.js CHANGED
@@ -3902,6 +3902,12 @@ const SUPERVISOR_PROBE_RESPONSE_TIMEOUT_MS = 2_000;
3902
3902
  // Hard cap on a single connection's inbound buffer, so a misbehaving client
3903
3903
  // can't grow the daemon's memory without bound with a newline-free frame.
3904
3904
  const SUPERVISOR_MAX_FRAME_BYTES = 1 << 20; // 1 MiB
3905
+ // How often the daemon re-samples worker activity to push a refreshed status to
3906
+ // attached consoles. The push is change-gated (see supervisorStatusSignature),
3907
+ // so a quiet fleet stays silent; only real transitions (idle↔busy, a new job,
3908
+ // restart/exit) reprint the table. `NANO_SUPERVISOR_MONITOR_MS=0` disables the
3909
+ // live refresh (falling back to the attach-time snapshot + lifecycle events).
3910
+ const SUPERVISOR_MONITOR_INTERVAL_MS = 1_000;
3905
3911
 
3906
3912
  // The `nano work` flags forwarded verbatim to each spawned child.
3907
3913
  // kind: 'value' → `--flag v`; 'boolean' → `--flag`; 'list' → repeated `--flag v`.
@@ -4184,6 +4190,33 @@ function summarizeSupervisorWorker(w, now = Date.now()) {
4184
4190
  };
4185
4191
  }
4186
4192
 
4193
+ /**
4194
+ * A stable fingerprint of the fleet's *observable* state for change detection.
4195
+ * Deliberately excludes ticking durations (uptimeMs, per-job sinceMs) so that a
4196
+ * merely-elapsing clock doesn't count as a change — only real transitions (a
4197
+ * worker going up/down, idle↔busy, picking up/finishing a job, a restart) alter
4198
+ * the signature. The daemon uses this to push a refreshed status to attached
4199
+ * consoles only when something actually changed, keeping a quiet fleet silent.
4200
+ * `workers` is an array of `summarizeSupervisorWorker` results.
4201
+ */
4202
+ function supervisorStatusSignature(workers) {
4203
+ const list = Array.isArray(workers) ? workers : [];
4204
+ return JSON.stringify(
4205
+ list.map((w) => [
4206
+ w.id,
4207
+ w.profile ?? '',
4208
+ w.state,
4209
+ w.pid ?? 0,
4210
+ Number(w.restarts) || 0,
4211
+ w.lastExit ?? '',
4212
+ w.activity ? w.activity.state : null,
4213
+ w.activity
4214
+ ? w.activity.jobs.map((j) => `${j.key}\u0000${j.type ?? ''}`).sort()
4215
+ : null,
4216
+ ]),
4217
+ );
4218
+ }
4219
+
4187
4220
  /** One-line JOB cell for a status row: the serviced job key, `idle`, or `-`. */
4188
4221
  function supervisorJobCell(w) {
4189
4222
  if (w.state !== 'running') return '-';
@@ -4338,6 +4371,10 @@ async function runSupervisorDaemon() {
4338
4371
  const workers = new Map();
4339
4372
  const attachClients = new Set();
4340
4373
  let shuttingDown = false;
4374
+ // Live-view monitor: tracks the last-broadcast fleet signature so we push a
4375
+ // refreshed status to attached consoles only on real change (see below).
4376
+ let monitorTimer = null;
4377
+ let lastMonitorSig = null;
4341
4378
 
4342
4379
  // Daemon-wide mutation serialization: `add`/`remove`/`restart` must not
4343
4380
  // interleave, or two clients racing the same worker could each spawn an
@@ -4501,11 +4538,15 @@ async function runSupervisorDaemon() {
4501
4538
  return [...workers.values()].filter((w) => w.profile === t).map((w) => w.id);
4502
4539
  };
4503
4540
 
4504
- const statusFrame = (final) => ({
4541
+ // `pub` lets a caller that has already sampled the fleet (e.g. the monitor
4542
+ // tick, which needs the snapshot to compute its change signature) reuse that
4543
+ // exact snapshot for the frame — so the broadcast payload is guaranteed to
4544
+ // match the signature that decided to send it, with no second re-sample.
4545
+ const statusFrame = (final, pub) => ({
4505
4546
  ok: true,
4506
4547
  type: 'status',
4507
4548
  daemon: { pid: process.pid, startedAt, socket: socketPath, logFile: daemonLogFile },
4508
- workers: [...workers.values()].map(workerPublic),
4549
+ workers: pub || [...workers.values()].map(workerPublic),
4509
4550
  ...(final ? { final: true } : {}),
4510
4551
  });
4511
4552
 
@@ -4515,6 +4556,7 @@ async function runSupervisorDaemon() {
4515
4556
  // Let any in-flight mutation finish before we snapshot the worker set, so
4516
4557
  // an add/restart racing the shutdown can't leave an orphaned child behind.
4517
4558
  try { await opQueue; } catch { /* mutation already logged */ }
4559
+ if (monitorTimer) { try { clearInterval(monitorTimer); } catch { /* ignore */ } monitorTimer = null; }
4518
4560
  dlog(`received ${signal || 'stop'} — stopping ${workers.size} worker(s)`);
4519
4561
  await Promise.all([...workers.keys()].map((id) => stopWorker(id)));
4520
4562
  broadcast({ type: 'event', event: 'daemon-stop' });
@@ -4655,6 +4697,37 @@ async function runSupervisorDaemon() {
4655
4697
  dlog(`supervisor daemon up (pid ${process.pid}) — control ${socketPath}`);
4656
4698
  persist();
4657
4699
 
4700
+ // Live-view refresh: periodically re-sample worker activity and push a fresh
4701
+ // status to attached consoles, but only when the fleet's observable state
4702
+ // actually changed since the last push (idle↔busy, a new/finished job, a
4703
+ // restart/exit). This keeps an attached `supervisor` console current without
4704
+ // spamming a quiet fleet. The signature always tracks the latest state (even
4705
+ // with no clients attached) so an idle-fleet attach — whose snapshot already
4706
+ // matches the tracked signature — won't provoke a redundant reprint for
4707
+ // everyone on the next tick. (A change that lands in the sub-tick window
4708
+ // *between* a tick and a fresh attach can still yield one extra identical
4709
+ // frame to the newcomer; that reprint is required to inform the already-
4710
+ // attached clients of the change, and is harmless — same content, re-rendered.)
4711
+ // Env-gated: NANO_SUPERVISOR_MONITOR_MS=0 disables; otherwise it's the cadence.
4712
+ const monitorMs = (() => {
4713
+ const raw = process.env.NANO_SUPERVISOR_MONITOR_MS;
4714
+ if (raw == null || raw === '') return SUPERVISOR_MONITOR_INTERVAL_MS;
4715
+ const n = Number(raw);
4716
+ return Number.isFinite(n) && n >= 0 ? Math.floor(n) : SUPERVISOR_MONITOR_INTERVAL_MS;
4717
+ })();
4718
+ if (monitorMs > 0) {
4719
+ lastMonitorSig = supervisorStatusSignature([...workers.values()].map(workerPublic));
4720
+ monitorTimer = setInterval(() => {
4721
+ if (shuttingDown) return;
4722
+ const pub = [...workers.values()].map(workerPublic);
4723
+ const sig = supervisorStatusSignature(pub);
4724
+ const changed = sig !== lastMonitorSig;
4725
+ lastMonitorSig = sig;
4726
+ if (changed && attachClients.size > 0) broadcast(statusFrame(false, pub));
4727
+ }, monitorMs);
4728
+ if (typeof monitorTimer.unref === 'function') monitorTimer.unref();
4729
+ }
4730
+
4658
4731
  // Keep the event loop alive indefinitely; the server holds it, but add an
4659
4732
  // explicit never-resolving guard so a transient server close can't exit us.
4660
4733
  await new Promise(() => {});
@@ -4986,10 +5059,12 @@ async function attachSupervisorConsole(state) {
4986
5059
  sock.write(encodeFrame({ op: 'attach' }));
4987
5060
 
4988
5061
  let buf = '';
5062
+ let rl = null;
4989
5063
  sock.on('data', (chunk) => {
4990
5064
  buf += chunk;
4991
5065
  const { frames, rest } = decodeFrames(buf);
4992
5066
  buf = rest;
5067
+ if (frames.length === 0) return;
4993
5068
  for (const frame of frames) {
4994
5069
  if (frame.type === 'status') {
4995
5070
  out('');
@@ -5010,9 +5085,13 @@ async function attachSupervisorConsole(state) {
5010
5085
  out(`! ${frame.error}`);
5011
5086
  }
5012
5087
  }
5088
+ // A pushed frame writes straight to stdout, stepping on the readline prompt
5089
+ // and any half-typed command. Re-render the prompt (preserving the input
5090
+ // buffer) so an async live-view refresh doesn't corrupt what the user typed.
5091
+ if (rl) { try { rl.prompt(true); } catch { /* ignore */ } }
5013
5092
  });
5014
5093
 
5015
- const rl = createReadline({ input: process.stdin, output: process.stdout, prompt: 'supervisor> ' });
5094
+ rl = createReadline({ input: process.stdin, output: process.stdout, prompt: 'supervisor> ' });
5016
5095
  rl.prompt();
5017
5096
 
5018
5097
  await new Promise((resolve) => {
@@ -6519,6 +6598,7 @@ export {
6519
6598
  formatDuration,
6520
6599
  summarizeSupervisorWorker,
6521
6600
  formatSupervisorStatus,
6601
+ supervisorStatusSignature,
6522
6602
  supervisorJobCell,
6523
6603
  supervisorWorkerActivityFile,
6524
6604
  WORK_FORWARD_FLAGS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c8ctl-plugin-nano",
3
- "version": "1.26.2",
3
+ "version": "1.26.3",
4
4
  "type": "module",
5
5
  "description": "c8ctl plugin to start, inspect, and stop a local Nano BPM (nanobpmn) cluster",
6
6
  "main": "c8ctl-plugin.js",
@@ -47,12 +47,12 @@
47
47
  "semantic-release": "^25.0.3"
48
48
  },
49
49
  "optionalDependencies": {
50
- "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.26.2",
51
- "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.26.2",
52
- "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.26.2",
53
- "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.26.2",
54
- "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.26.2",
55
- "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.26.2",
56
- "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.26.2"
50
+ "@nanobpm/c8ctl-plugin-nano-darwin-arm64": "1.26.3",
51
+ "@nanobpm/c8ctl-plugin-nano-darwin-x64": "1.26.3",
52
+ "@nanobpm/c8ctl-plugin-nano-linux-x64": "1.26.3",
53
+ "@nanobpm/c8ctl-plugin-nano-linux-arm64": "1.26.3",
54
+ "@nanobpm/c8ctl-plugin-nano-linux-armv7": "1.26.3",
55
+ "@nanobpm/c8ctl-plugin-nano-linux-armv6": "1.26.3",
56
+ "@nanobpm/c8ctl-plugin-nano-win32-x64": "1.26.3"
57
57
  }
58
58
  }