mikser-io 10.5.0 → 10.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "10.5.0",
3
+ "version": "10.6.0",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
package/src/engine.js CHANGED
@@ -25,8 +25,8 @@ import Queue from 'p-queue'
25
25
  import packageInfo from '../package.json' with { type: 'json' }
26
26
  import { attachServerCliOptions, setupServer } from './server.js'
27
27
  import {
28
- createMikserLogger, setLogLevel, rememberBaseLevel, installLogLevel, resetLogLevel,
29
- applyInstalledLogLevel, installedLogLevel, LOG_LEVELS, INSTALLED_LOG_TTL_MS,
28
+ createMikserLogger, rememberBaseLevel, applyLogRequest,
29
+ LOG_LEVELS, INSTALLED_LOG_TTL_MS,
30
30
  } from './logger.js'
31
31
  import { inputHashOf } from './utils.js'
32
32
  import { createTrack, mergeTrack } from './track.js'
@@ -685,14 +685,14 @@ The full version, with what each code means: docs/diagnostics.md`)
685
685
  // logger — the whole reason --debug did nothing.
686
686
  runtime.options.info = true
687
687
  const asked = runtime.options.log
688
- if (asked) {
689
- if (!LOG_LEVELS.includes(asked)) {
690
- throw new Error(`--log ${asked}: no such level. Levels: ${LOG_LEVELS.join(', ')}`)
691
- }
692
- setLogLevel(asked)
693
- // A bar on top of debug output is noise, and silent means silent.
694
- if (asked === 'trace' || asked === 'debug' || asked === 'silent') runtime.options.info = false
695
- }
688
+ // The same call the instance makes for a forwarded request — see
689
+ // applyLogRequest. Two implementations drifted within one commit.
690
+ const logRefusal = applyLogRequest({
691
+ log: asked,
692
+ logInstall: runtime.options.logInstall,
693
+ logReset: runtime.options.logReset,
694
+ })
695
+ if (logRefusal) throw new Error(logRefusal)
696
696
  rememberBaseLevel(asked && LOG_LEVELS.includes(asked) ? asked : 'info')
697
697
 
698
698
  // Resolve folders inside onInitialize so journal.js and
package/src/instance.js CHANGED
@@ -38,7 +38,7 @@ import { onLoaded } from './lifecycle.js'
38
38
  import { renderErrorCount } from './report.js'
39
39
  import { runReportOnly } from './engine.js'
40
40
  import {
41
- setLogLevel, installLogLevel, resetLogLevel, restingLogLevel,
41
+ setLogLevel, restingLogLevel, applyLogRequest, LOG_LEVELS,
42
42
  } from './logger.js'
43
43
  import { emitReport } from './report.js'
44
44
  import { pluginOptionsFrom } from './cli.js'
@@ -240,6 +240,17 @@ function captureOutput(onChunk) {
240
240
  // than by content hash — the hash would require the client to import its
241
241
  // config, which is most of the startup forwarding exists to skip, and it is
242
242
  // not what tells the two apart. Different configs are different files.
243
+ // Does this request name a level that does not exist? Pure, so the refusal
244
+ // can be decided before anything is applied.
245
+ function logRequestRefusal(request) {
246
+ for (const [flag, level] of [['--log', request?.log], ['--log-install', request?.logInstall]]) {
247
+ if (level !== undefined && level !== null && !LOG_LEVELS.includes(level)) {
248
+ return `${flag} ${level}: no such level. Levels: ${LOG_LEVELS.join(', ')}`
249
+ }
250
+ }
251
+ return null
252
+ }
253
+
243
254
  function configMismatch(theirs) {
244
255
  if (!theirs) return null
245
256
  const mine = path.resolve(runtime.options.config ?? 'mikser.config.js')
@@ -369,6 +380,26 @@ function refuseClear(socket, request) {
369
380
  return true
370
381
  }
371
382
 
383
+ // A level the instance cannot use.
384
+ //
385
+ // Validated where the request ARRIVES rather than where it is applied, because
386
+ // applying it is exactly what a bad level cannot do: setLogLevel returns false
387
+ // and, before this, nobody read it — so `--log chatty` exited 1 locally and
388
+ // built normally with a watcher up. The refusal frame is how the instance says
389
+ // no to everything else, and a client that mistypes a level deserves the same
390
+ // answer whether or not something happens to be listening.
391
+ function refuseLogLevel(socket, request) {
392
+ const refusal = logRequestRefusal(request)
393
+ if (!refusal) return false
394
+ frame(socket, {
395
+ type: 'refused',
396
+ reason: refusal,
397
+ detail: 'The instance would otherwise have built normally and said nothing, which is the '
398
+ + 'forwarded-versus-local split this surface exists to remove.',
399
+ })
400
+ return true
401
+ }
402
+
372
403
  // Which process to restart.
373
404
  //
374
405
  // "Restart it" is only actionable if you know which `it` — and the machine
@@ -428,15 +459,25 @@ async function withRequestOutput(request, run) {
428
459
  // a watcher's own cycle before or after keeps the level it was running at.
429
460
  // An installed level is different and deliberately not touched here — it
430
461
  // outlives the request, which is its whole point.
431
- if (request.logReset) resetLogLevel()
432
- if (request.logInstall) installLogLevel(request.logInstall)
433
- if (request.log) setLogLevel(request.log)
434
-
435
- for (const key of ['json', 'tool', 'tools', 'requested']) {
462
+ // Captured BEFORE applyLogRequest, which writes `info`.
463
+ //
464
+ // `info` is here because --log silent turns the progress bar off through
465
+ // it, and without a restore one silent request left the instance with no
466
+ // bar for the rest of its life. Capturing after the call would have
467
+ // recorded the value the call just wrote and restored nothing — the same
468
+ // shape of mistake, one line further on.
469
+ for (const key of ['json', 'tool', 'tools', 'requested', 'info']) {
436
470
  prior[key] = runtime.options[key]
437
471
  if (request[key]) runtime.options[key] = request[key]
438
472
  }
439
473
 
474
+ // The same call the argv path makes — see applyLogRequest. The level was
475
+ // validated at arrival by refuseLogLevel, so a refusal here is impossible;
476
+ // it is asserted rather than ignored, because "cannot happen" is how the
477
+ // first version of this silently accepted a bad level.
478
+ const refusal = applyLogRequest(request)
479
+ if (refusal) throw new Error(refusal)
480
+
440
481
  // Whatever the client's argv said about a PLUGIN's options.
441
482
  //
442
483
  // The instance parsed its own argv and never saw the client's, so a flag a
@@ -558,6 +599,7 @@ export function serveInstance() {
558
599
  // writes anything.
559
600
  if (refuseUnknownFlags(socket, request)) return
560
601
  if (refuseClear(socket, request)) return
602
+ if (refuseLogLevel(socket, request)) return
561
603
  const wrongConfig = configMismatch(request.config)
562
604
  if (wrongConfig) return refuseConfig(socket, request, wrongConfig)
563
605
  const movedFile = await configStale()
package/src/logger.js CHANGED
@@ -491,16 +491,49 @@ export function trackProgress(name, total) {
491
491
  if (!name || !total) return
492
492
  const logger = useLogger()
493
493
  logger.debug('%s started: %d', name, total)
494
- if (!process.stdout.isTTY || !runtime.options.info) return
495
- currentBar = { name, total, value: 0, started: Date.now() }
496
- ensureGauge().show({ section: name, subsection: `0/${total}` }, 0)
494
+ // The bar writes to stdout, and stdout is where --json and --tool put
495
+ // their DOCUMENT. Forwarded, those writes are captured and framed to the
496
+ // client, so the gauge landed inside the JSON:
497
+ // `^[[?25lDocuments import: >416/800` at byte 0, and JSON.parse threw —
498
+ // 4 runs in 10 at the default level on an 800-document corpus.
499
+ //
500
+ // Checked HERE rather than through runtime.options.info, because this is
501
+ // the actual invariant and info is a preference. A preference can be
502
+ // forgotten on a path; an invariant stated at the one place a bar starts
503
+ // cannot.
504
+ const carriesDocument = runtime.options?.json || runtime.options?.tool || runtime.options?.tools
505
+
506
+ // TRACKED always, DRAWN only where a bar belongs.
507
+ //
508
+ // The two used to be one decision, so anything that could not draw also
509
+ // stopped counting — and `stopProgress` returns early without a bar, which
510
+ // is where the "finished: N in Ns" line comes from. A piped build
511
+ // therefore reported no phase timings at all, and suppressing the bar for
512
+ // --json would have extended that to every machine reading the output.
513
+ // Losing the graphics is the point; losing the information is not.
514
+ const drawn = !carriesDocument && Boolean(process.stdout.isTTY) && Boolean(runtime.options.info)
515
+ currentBar = { name, total, value: 0, started: Date.now(), drawn, milestone: 0 }
516
+ if (drawn) ensureGauge().show({ section: name, subsection: `0/${total}` }, 0)
517
+ else logger.info({ code: 'progress', phase: name, total, value: 0 }, '%s: 0/%d', name, total)
497
518
  }
498
519
 
499
520
  export function updateProgress() {
500
521
  if (!currentBar) return
501
522
  currentBar.value++
502
- const { name, total, value } = currentBar
503
- gauge?.show({ section: name, subsection: `${value}/${total}` }, value / total)
523
+ const { name, total, value, drawn } = currentBar
524
+ if (drawn) {
525
+ gauge?.show({ section: name, subsection: `${value}/${total}` }, value / total)
526
+ } else {
527
+ // Quartiles, not every item: a bar redraws in place and costs one
528
+ // line, a log record does not. 800 documents is 800 lines of noise if
529
+ // this counts the way the bar does.
530
+ const reached = Math.floor((value / total) * 4)
531
+ if (reached > currentBar.milestone && value < total) {
532
+ currentBar.milestone = reached
533
+ useLogger()?.info({ code: 'progress', phase: name, total, value },
534
+ '%s: %d/%d', name, value, total)
535
+ }
536
+ }
504
537
  if (value >= total) stopProgress()
505
538
  }
506
539
 
@@ -509,11 +542,15 @@ export function stopProgress() {
509
542
  const logger = useLogger()
510
543
  const { name, total, value, started } = currentBar
511
544
  gauge?.hide()
545
+ // Structured either way, so a machine reading --json's stderr gets the
546
+ // same facts a person reads off the bar.
512
547
  if (value < total) {
513
- logger.warn('%s unfinished: %d', name, total - value)
548
+ logger.warn({ code: 'progress-unfinished', phase: name, total, value, missing: total - value },
549
+ '%s unfinished: %d', name, total - value)
514
550
  } else {
515
- const elapsed = Math.round((Date.now() - started) / 1000)
516
- logger.info('%s finished: %d %ds', name, total, elapsed)
551
+ const ms = Date.now() - started
552
+ logger.info({ code: 'progress-finished', phase: name, total, ms },
553
+ '%s finished: %d %ds', name, total, Math.round(ms / 1000))
517
554
  }
518
555
  currentBar = null
519
556
  }
@@ -528,6 +565,38 @@ export function updateProgressDetails(details) {
528
565
  )
529
566
  }
530
567
 
568
+ // What a caller asked for about logging, applied the same way from argv and
569
+ // from a forwarded request.
570
+ //
571
+ // It was written twice and the copies drifted immediately: the argv path threw
572
+ // on an unknown level and set `info`, the forwarded path called setLogLevel and
573
+ // ignored the false it returns. So `--log chatty` exited 1 locally and built
574
+ // normally with a watcher up, and `--log silent` left the progress bar running
575
+ // on an instance. The same forwarded/local split --json and --force each had,
576
+ // and this feature's own argument against itself: a flag that lies is worse
577
+ // than a flag that is missing.
578
+ //
579
+ // Returns an error STRING rather than throwing, because the two callers need
580
+ // different things from a failure — argv throws, the instance refuses over the
581
+ // socket — and a shared implementation should not decide that for them.
582
+ export function applyLogRequest({ log, logInstall, logReset } = {}) {
583
+ for (const [flag, level] of [['--log', log], ['--log-install', logInstall]]) {
584
+ if (level !== undefined && level !== null && !LOG_LEVELS.includes(level)) {
585
+ return `${flag} ${level}: no such level. Levels: ${LOG_LEVELS.join(', ')}`
586
+ }
587
+ }
588
+ if (logReset) resetLogLevel()
589
+ if (logInstall) installLogLevel(logInstall)
590
+ if (log) setLogLevel(log)
591
+
592
+ // A bar on top of debug output is noise, and silent means silent.
593
+ const level = log || logInstall
594
+ if (level === 'trace' || level === 'debug' || level === 'silent') {
595
+ runtime.options.info = false
596
+ }
597
+ return null
598
+ }
599
+
531
600
  // An installed level, disclosed and expired, once per cycle.
532
601
  //
533
602
  // Disclosed for the reason an installed command is: it is state left on a live