mikser-io 10.6.0 → 10.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/package.json +1 -1
- package/src/engine.js +30 -0
- package/src/instance.js +32 -3
- package/src/journal.js +1 -2
- package/src/logger.js +60 -11
- package/src/plugins/files.js +1 -1
- package/src/plugins/observer.js +2 -2
- package/src/plugins/resources.js +2 -2
- package/src/source.js +1 -1
package/package.json
CHANGED
package/src/engine.js
CHANGED
|
@@ -1593,6 +1593,16 @@ The full version, with what each code means: docs/diagnostics.md`)
|
|
|
1593
1593
|
// undefined here. The logger has no such problem — it writes during the
|
|
1594
1594
|
// run, by which time options exist.
|
|
1595
1595
|
const quietStdout = ['--json', '--tool', '--tools'].some(flag => process.argv.includes(flag))
|
|
1596
|
+
// The banner is an info record, so a level above info must not print it.
|
|
1597
|
+
//
|
|
1598
|
+
// `--log silent` printed the version line and nothing else — the one line
|
|
1599
|
+
// the flag most obviously promises to remove — because this runs before
|
|
1600
|
+
// the level is applied. Only the FORWARDED path was ever silent, and that
|
|
1601
|
+
// is the path the test exercises, so the test could not see it.
|
|
1602
|
+
//
|
|
1603
|
+
// argv again, for the reason quietStdout reads it: commander parses in a
|
|
1604
|
+
// lifecycle hook, which runs after setup() returns.
|
|
1605
|
+
if (!argvWantsInfo()) return runtime
|
|
1596
1606
|
// Through the logger when nobody is watching, so it gets a timestamp.
|
|
1597
1607
|
//
|
|
1598
1608
|
// This line marks a process start, which in a supervisor's log is the
|
|
@@ -1625,6 +1635,26 @@ The full version, with what each code means: docs/diagnostics.md`)
|
|
|
1625
1635
|
return runtime
|
|
1626
1636
|
}
|
|
1627
1637
|
|
|
1638
|
+
// The level asked for on the command line, before commander has parsed.
|
|
1639
|
+
// Both spellings, both flags: `--log warn` and `--log=warn`, and
|
|
1640
|
+
// `--log-install`, which on a run with no instance to forward to sets the
|
|
1641
|
+
// level for this process.
|
|
1642
|
+
function argvLogLevel() {
|
|
1643
|
+
for (const flag of ['--log', '--log-install', '-l']) {
|
|
1644
|
+
const at = process.argv.indexOf(flag)
|
|
1645
|
+
if (at >= 0 && process.argv[at + 1]) return process.argv[at + 1]
|
|
1646
|
+
const inline = process.argv.find(arg => arg.startsWith(`${flag}=`))
|
|
1647
|
+
if (inline) return inline.slice(flag.length + 1)
|
|
1648
|
+
}
|
|
1649
|
+
return null
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function argvWantsInfo() {
|
|
1653
|
+
const level = argvLogLevel()
|
|
1654
|
+
if (!level || !LOG_LEVELS.includes(level)) return true
|
|
1655
|
+
return LOG_LEVELS.indexOf(level) <= LOG_LEVELS.indexOf('info')
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1628
1658
|
export function useLogger() {
|
|
1629
1659
|
return runtime.engine?.logger
|
|
1630
1660
|
}
|
package/src/instance.js
CHANGED
|
@@ -218,10 +218,29 @@ let server = null
|
|
|
218
218
|
// both into one undifferentiated stream throws that away, and the client can
|
|
219
219
|
// only guess — it guessed stderr, so every forwarded document landed where no
|
|
220
220
|
// consumer looks while the command exited 0.
|
|
221
|
-
|
|
221
|
+
// `echoStdout: false` frames the chunk to the client and writes NOTHING
|
|
222
|
+
// locally.
|
|
223
|
+
//
|
|
224
|
+
// The instance wears the client's output so an operator watching it sees what
|
|
225
|
+
// a forwarded request did — right for log lines, wrong for a DOCUMENT. A
|
|
226
|
+
// --json request put its 364-line report into the instance's own console as
|
|
227
|
+
// well as onto the client's stdout, and under pm2 that is 364 lines in the out
|
|
228
|
+
// log for every report an agent asks for.
|
|
229
|
+
//
|
|
230
|
+
// The seam is exact rather than a guess: under --json / --tool the logger
|
|
231
|
+
// writes to stderr and stdout carries only the document (see logger.js), so
|
|
232
|
+
// stdout chunks during a document-carrying request ARE the document.
|
|
233
|
+
function captureOutput(onChunk, { echoStdout = true } = {}) {
|
|
222
234
|
const originals = [process.stdout.write, process.stderr.write]
|
|
223
235
|
const patch = (stream, original, name) => function (chunk, encoding, callback) {
|
|
224
236
|
try { onChunk(typeof chunk === 'string' ? chunk : chunk.toString(), name) } catch { /* client gone */ }
|
|
237
|
+
if (!echoStdout && name === 'stdout') {
|
|
238
|
+
// write()'s callback is the second argument when encoding is
|
|
239
|
+
// omitted. Dropped, a caller awaiting the drain never resumes.
|
|
240
|
+
const done = typeof encoding === 'function' ? encoding : callback
|
|
241
|
+
if (typeof done === 'function') process.nextTick(done)
|
|
242
|
+
return true
|
|
243
|
+
}
|
|
225
244
|
return original.call(stream, chunk, encoding, callback)
|
|
226
245
|
}
|
|
227
246
|
process.stdout.write = patch(process.stdout, originals[0], 'stdout')
|
|
@@ -242,6 +261,12 @@ function captureOutput(onChunk) {
|
|
|
242
261
|
// not what tells the two apart. Different configs are different files.
|
|
243
262
|
// Does this request name a level that does not exist? Pure, so the refusal
|
|
244
263
|
// can be decided before anything is applied.
|
|
264
|
+
// Does this request's answer go on stdout as a document?
|
|
265
|
+
// The same trio logger.js gates the progress bar on, for the same reason.
|
|
266
|
+
function carriesDocument(request) {
|
|
267
|
+
return Boolean(request?.json || request?.tool || request?.tools)
|
|
268
|
+
}
|
|
269
|
+
|
|
245
270
|
function logRequestRefusal(request) {
|
|
246
271
|
for (const [flag, level] of [['--log', request?.log], ['--log-install', request?.logInstall]]) {
|
|
247
272
|
if (level !== undefined && level !== null && !LOG_LEVELS.includes(level)) {
|
|
@@ -296,7 +321,9 @@ async function configStale() {
|
|
|
296
321
|
// the only process that can answer correctly. Same guards as a build: wrong
|
|
297
322
|
// config refuses, drifted config refuses.
|
|
298
323
|
async function serveReport(socket, request, logger) {
|
|
299
|
-
const restore = captureOutput(
|
|
324
|
+
const restore = captureOutput(
|
|
325
|
+
(chunk, stream) => frame(socket, { type: 'log', chunk, stream }),
|
|
326
|
+
{ echoStdout: !carriesDocument(request) })
|
|
300
327
|
let code = 0
|
|
301
328
|
try {
|
|
302
329
|
code = await withRequestOutput(request, () => runReportOnly(request)) ?? 0
|
|
@@ -501,7 +528,9 @@ async function withRequestOutput(request, run) {
|
|
|
501
528
|
}
|
|
502
529
|
|
|
503
530
|
async function serveBuild(socket, request, logger) {
|
|
504
|
-
const restore = captureOutput(
|
|
531
|
+
const restore = captureOutput(
|
|
532
|
+
(chunk, stream) => frame(socket, { type: 'log', chunk, stream }),
|
|
533
|
+
{ echoStdout: !carriesDocument(request) })
|
|
505
534
|
let code = 0
|
|
506
535
|
try {
|
|
507
536
|
// Fire the pending debounce rather than waiting it out.
|
package/src/journal.js
CHANGED
|
@@ -227,9 +227,8 @@ export async function* useJournal(name, operations, signal) {
|
|
|
227
227
|
stopProgress()
|
|
228
228
|
throw new AbortError()
|
|
229
229
|
}
|
|
230
|
-
updateProgress()
|
|
231
|
-
|
|
232
230
|
const entry = rowToEntry(row)
|
|
231
|
+
updateProgress(entry.entity?.id)
|
|
233
232
|
const originalEntity = row.entity // already a JSON string
|
|
234
233
|
|
|
235
234
|
yield entry
|
package/src/logger.js
CHANGED
|
@@ -28,6 +28,7 @@ import pino from 'pino'
|
|
|
28
28
|
import pretty from 'pino-pretty'
|
|
29
29
|
import Gauge from 'gauge'
|
|
30
30
|
import { Writable } from 'node:stream'
|
|
31
|
+
import path from 'node:path'
|
|
31
32
|
import runtime from './runtime.js'
|
|
32
33
|
import { useLogger } from './engine.js'
|
|
33
34
|
import { onLoad, onFinalized } from './lifecycle.js'
|
|
@@ -482,10 +483,48 @@ onLoad(() => {
|
|
|
482
483
|
runtime.engine.logger = createMikserLogger(level)
|
|
483
484
|
})
|
|
484
485
|
|
|
485
|
-
// Progress API
|
|
486
|
-
//
|
|
487
|
-
//
|
|
488
|
-
|
|
486
|
+
// Progress API: trackProgress / updateProgress / stopProgress /
|
|
487
|
+
// updateProgressDetails. A gauge where a terminal is watching, coded records
|
|
488
|
+
// where one is not, and nothing at all for a phase that took no time.
|
|
489
|
+
|
|
490
|
+
// Long phases only, and long means TIME.
|
|
491
|
+
//
|
|
492
|
+
// A record per quartile is right for 800 documents and absurd for 5. Eleven
|
|
493
|
+
// of a plain build's thirteen phases carry exactly five items, so reporting
|
|
494
|
+
// them the way a bar counts took a piped no-op build from 32 lines to 97 —
|
|
495
|
+
// sixty-five progress records, five in the same millisecond to say a phase
|
|
496
|
+
// did nothing. That is written into the log a deployment keeps, which is the
|
|
497
|
+
// log the installed-level expiry exists to protect.
|
|
498
|
+
//
|
|
499
|
+
// A minimum TOTAL would fix the count and get the other half wrong: four PDFs
|
|
500
|
+
// through Chrome is a small phase and a slow one, and it is exactly the phase
|
|
501
|
+
// worth narrating. Elapsed time is what "long" means, it needs no per-phase
|
|
502
|
+
// tuning, and it applies to the drawn path too — `finished: 5 0s` is the same
|
|
503
|
+
// non-information in a terminal, and its own `0s` says so.
|
|
504
|
+
export const PROGRESS_MIN_MS = 1000
|
|
505
|
+
|
|
506
|
+
// Which item, not just how far. A phase name alone says a build is doing
|
|
507
|
+
// something; the thing a stuck build needs to say is WHAT it is stuck on.
|
|
508
|
+
function progressDetail(detail) {
|
|
509
|
+
if (detail === null || detail === undefined || detail === '') return null
|
|
510
|
+
const text = String(detail)
|
|
511
|
+
const root = runtime.options?.workingFolder
|
|
512
|
+
if (!root || !path.isAbsolute(text)) return text
|
|
513
|
+
// An entity id LOOKS absolute and is not a filesystem path.
|
|
514
|
+
// `/documents/p3706.md` relativised against the working folder becomes
|
|
515
|
+
// `../../../documents/p3706.md`, which names nothing anyone can act on.
|
|
516
|
+
// So shorten only what is genuinely INSIDE the folder, and leave every
|
|
517
|
+
// other string exactly as the call site handed it over.
|
|
518
|
+
const relative = path.relative(root, text)
|
|
519
|
+
return relative.startsWith('..') ? text : relative
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function emitProgress({ name, total, value, detail }) {
|
|
523
|
+
const at = progressDetail(detail)
|
|
524
|
+
const fields = { code: 'progress', phase: name, total, value }
|
|
525
|
+
if (at) useLogger()?.info({ ...fields, detail: at }, '%s: %d/%d — %s', name, value, total, at)
|
|
526
|
+
else useLogger()?.info(fields, '%s: %d/%d', name, value, total)
|
|
527
|
+
}
|
|
489
528
|
|
|
490
529
|
export function trackProgress(name, total) {
|
|
491
530
|
if (!name || !total) return
|
|
@@ -512,14 +551,18 @@ export function trackProgress(name, total) {
|
|
|
512
551
|
// --json would have extended that to every machine reading the output.
|
|
513
552
|
// Losing the graphics is the point; losing the information is not.
|
|
514
553
|
const drawn = !carriesDocument && Boolean(process.stdout.isTTY) && Boolean(runtime.options.info)
|
|
515
|
-
currentBar = { name, total, value: 0, started: Date.now(), drawn, milestone: 0 }
|
|
554
|
+
currentBar = { name, total, value: 0, started: Date.now(), drawn, milestone: 0, detail: null }
|
|
516
555
|
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)
|
|
518
556
|
}
|
|
519
557
|
|
|
520
|
-
export function updateProgress() {
|
|
558
|
+
export function updateProgress(detail) {
|
|
521
559
|
if (!currentBar) return
|
|
522
560
|
currentBar.value++
|
|
561
|
+
// The call site hands over whatever identifies the item it is on — a
|
|
562
|
+
// path, an id — and pays one assignment for it. Formatting happens at
|
|
563
|
+
// emit time, at most four times a phase, so naming the work costs the
|
|
564
|
+
// 14k-entity import nothing.
|
|
565
|
+
if (detail !== undefined) currentBar.detail = detail
|
|
523
566
|
const { name, total, value, drawn } = currentBar
|
|
524
567
|
if (drawn) {
|
|
525
568
|
gauge?.show({ section: name, subsection: `${value}/${total}` }, value / total)
|
|
@@ -527,11 +570,14 @@ export function updateProgress() {
|
|
|
527
570
|
// Quartiles, not every item: a bar redraws in place and costs one
|
|
528
571
|
// line, a log record does not. 800 documents is 800 lines of noise if
|
|
529
572
|
// this counts the way the bar does.
|
|
573
|
+
//
|
|
574
|
+
// The milestone advances whether or not the record is emitted. Held
|
|
575
|
+
// back, every quartile the phase passed under the threshold would fire
|
|
576
|
+
// in a burst the moment it crossed.
|
|
530
577
|
const reached = Math.floor((value / total) * 4)
|
|
531
578
|
if (reached > currentBar.milestone && value < total) {
|
|
532
579
|
currentBar.milestone = reached
|
|
533
|
-
|
|
534
|
-
'%s: %d/%d', name, value, total)
|
|
580
|
+
if (Date.now() - currentBar.started >= PROGRESS_MIN_MS) emitProgress(currentBar)
|
|
535
581
|
}
|
|
536
582
|
}
|
|
537
583
|
if (value >= total) stopProgress()
|
|
@@ -541,14 +587,17 @@ export function stopProgress() {
|
|
|
541
587
|
if (!currentBar) return
|
|
542
588
|
const logger = useLogger()
|
|
543
589
|
const { name, total, value, started } = currentBar
|
|
590
|
+
const ms = Date.now() - started
|
|
544
591
|
gauge?.hide()
|
|
545
592
|
// Structured either way, so a machine reading --json's stderr gets the
|
|
546
593
|
// same facts a person reads off the bar.
|
|
594
|
+
//
|
|
595
|
+
// A phase that did not finish is worth a line at any duration — it is a
|
|
596
|
+
// warning, not progress, and the whole point is that it is unexpected.
|
|
547
597
|
if (value < total) {
|
|
548
598
|
logger.warn({ code: 'progress-unfinished', phase: name, total, value, missing: total - value },
|
|
549
599
|
'%s unfinished: %d', name, total - value)
|
|
550
|
-
} else {
|
|
551
|
-
const ms = Date.now() - started
|
|
600
|
+
} else if (ms >= PROGRESS_MIN_MS) {
|
|
552
601
|
logger.info({ code: 'progress-finished', phase: name, total, ms },
|
|
553
602
|
'%s finished: %d %ds', name, total, Math.round(ms / 1000))
|
|
554
603
|
}
|
package/src/plugins/files.js
CHANGED
|
@@ -228,7 +228,7 @@ export function files(options = {}) {
|
|
|
228
228
|
// nothing to emit. Progress ticks either way — a gated file
|
|
229
229
|
// was still looked at.
|
|
230
230
|
const newChecksum = await gateChecksum(source, id, { priorChecksums })
|
|
231
|
-
updateProgress()
|
|
231
|
+
updateProgress(id)
|
|
232
232
|
if (newChecksum === null) return
|
|
233
233
|
await createEntity({
|
|
234
234
|
id,
|
package/src/plugins/observer.js
CHANGED
|
@@ -100,7 +100,7 @@ export function observer(options = {}) {
|
|
|
100
100
|
synced++
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
updateProgress()
|
|
103
|
+
updateProgress(meta.id)
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
const entitiesToRemove = await findEntities({
|
|
@@ -114,7 +114,7 @@ export function observer(options = {}) {
|
|
|
114
114
|
for (let entity of entitiesToRemove) {
|
|
115
115
|
deleteEntity(entity)
|
|
116
116
|
removed++
|
|
117
|
-
updateProgress()
|
|
117
|
+
updateProgress(entity.id)
|
|
118
118
|
}
|
|
119
119
|
if (synced || removed) {
|
|
120
120
|
logger.debug('Syncing api [%s] synced: %d, removed: %d', collection, synced, removed)
|
package/src/plugins/resources.js
CHANGED
|
@@ -145,7 +145,7 @@ export function resources(options = {}) {
|
|
|
145
145
|
logger.error('Resource error: %s %s %s', entity.id, resource, err.message)
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
-
updateProgress()
|
|
148
|
+
updateProgress(resource)
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
const resourceFiles = await globby('**/*', { cwd: runtime.options.resourcesFolder })
|
|
@@ -226,7 +226,7 @@ export function resources(options = {}) {
|
|
|
226
226
|
checksum: await checksum(resource)
|
|
227
227
|
})
|
|
228
228
|
}
|
|
229
|
-
updateProgress()
|
|
229
|
+
updateProgress(resource)
|
|
230
230
|
}, { concurrency: 10, signal })
|
|
231
231
|
count && logger.info('Downloaded: %d', count)
|
|
232
232
|
}
|
package/src/source.js
CHANGED
|
@@ -431,7 +431,7 @@ export function useSource(core, options) {
|
|
|
431
431
|
// is never the bottleneck.
|
|
432
432
|
await pMap(files, async (file) => {
|
|
433
433
|
await registerFile(file, { logger, scanned, stats: scanStats, priorChecksums })
|
|
434
|
-
if (phase === 'import') updateProgress()
|
|
434
|
+
if (phase === 'import') updateProgress(file)
|
|
435
435
|
}, { concurrency: SCAN_CONCURRENCY })
|
|
436
436
|
|
|
437
437
|
scanStats.deleted = await sweepDeleted(collection, scanned, async (e) => {
|