mikser-io 10.1.0 → 10.3.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/database/index.js +157 -12
- package/src/engine.js +25 -1
- package/src/invalidation.js +7 -0
- package/src/logger.js +36 -8
package/package.json
CHANGED
package/src/database/index.js
CHANGED
|
@@ -44,11 +44,12 @@
|
|
|
44
44
|
|
|
45
45
|
import path from 'node:path'
|
|
46
46
|
import { mkdirSync, unlinkSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
47
|
+
import { createHash } from 'node:crypto'
|
|
47
48
|
import Database from 'better-sqlite3'
|
|
48
49
|
import runtime from '../runtime.js'
|
|
49
50
|
import { isReportOnlyRun } from '../tools.js'
|
|
50
51
|
import { reportWipe } from '../report.js'
|
|
51
|
-
import { onLoaded } from '../lifecycle.js'
|
|
52
|
+
import { onLoaded, onFinalized } from '../lifecycle.js'
|
|
52
53
|
import packageInfo from '../../package.json' with { type: 'json' }
|
|
53
54
|
|
|
54
55
|
// Local logger resolver — same one-liner engine.js exports as
|
|
@@ -232,6 +233,43 @@ export function useDatabase() {
|
|
|
232
233
|
return db
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
// What the cache's SHAPE is, as a value that changes only when the cache
|
|
237
|
+
// stops being reusable.
|
|
238
|
+
//
|
|
239
|
+
// It used to be the package version, so every release wiped every
|
|
240
|
+
// deployment's cache and rebuilt from cold — a patch that touched a README
|
|
241
|
+
// discarded 14k entities and re-rendered a site. The version was standing in
|
|
242
|
+
// for "something might have changed", which is true of every release and
|
|
243
|
+
// therefore says nothing.
|
|
244
|
+
//
|
|
245
|
+
// Two things actually invalidate a cache, and both are in here:
|
|
246
|
+
//
|
|
247
|
+
// the SQL — a column, index or table that moved. Hashed from the
|
|
248
|
+
// registered scripts themselves, so it cannot drift from what
|
|
249
|
+
// is applied: the fingerprint and the schema come from one
|
|
250
|
+
// string.
|
|
251
|
+
// the SHAPE — what the rows MEAN, when the SQL is untouched. A change to
|
|
252
|
+
// how inputHash is computed, to the edge kinds in a
|
|
253
|
+
// refClosure, to what a destination is relative to. No parser
|
|
254
|
+
// can see these, so they are a number someone bumps.
|
|
255
|
+
//
|
|
256
|
+
// BUMP DERIVED_SHAPE when a release changes the meaning of anything already
|
|
257
|
+
// stored. Getting that wrong is a silent stale cache, which is the failure
|
|
258
|
+
// this whole subsystem keeps producing — so when in doubt, bump. A needless
|
|
259
|
+
// wipe costs one cold rebuild; a missed one costs a site serving wrong
|
|
260
|
+
// output with every signal green.
|
|
261
|
+
const DERIVED_SHAPE = 1
|
|
262
|
+
|
|
263
|
+
// `<shape>:<sql-hash>` rather than a bare hash: the stored value is read by
|
|
264
|
+
// a human when a wipe happens, and the two halves say WHICH moved.
|
|
265
|
+
function cacheFingerprint(registered) {
|
|
266
|
+
const sql = [...registered.entries()]
|
|
267
|
+
.map(([name, value]) => `${name}\n${schemaEntry(value).sql}`)
|
|
268
|
+
.sort()
|
|
269
|
+
.join('\n')
|
|
270
|
+
return `${DERIVED_SHAPE}:${createHash('sha1').update(sql).digest('hex').slice(0, 12)}`
|
|
271
|
+
}
|
|
272
|
+
|
|
235
273
|
// Build a sqlite-backed database handle. Exported so tests can exercise
|
|
236
274
|
// the lifecycle and transaction semantics in isolation (without driving
|
|
237
275
|
// the full onLoaded chain). The runtime path uses this internally from
|
|
@@ -242,6 +280,10 @@ export function createSqliteDatabase({
|
|
|
242
280
|
// `--clear` asks for. Removes the file; nothing durable is in it.
|
|
243
281
|
forceWipe = false,
|
|
244
282
|
}) {
|
|
283
|
+
// Set at open, written at the first successful finalize. See the note
|
|
284
|
+
// where it is assigned.
|
|
285
|
+
let pendingStamp = null
|
|
286
|
+
|
|
245
287
|
// Tests inject their own provisioners; the runtime path falls
|
|
246
288
|
// through to the module-level `provisioners` array that plugins
|
|
247
289
|
// populate via onProvision() at module-eval.
|
|
@@ -292,6 +334,11 @@ export function createSqliteDatabase({
|
|
|
292
334
|
|
|
293
335
|
const stmtMeta = handle.prepare('SELECT value FROM mikser_meta WHERE key = ?')
|
|
294
336
|
const recorded = stmtMeta.get('schema_version')?.value
|
|
337
|
+
// The identity of the cache's shape, not of the release that wrote
|
|
338
|
+
// it. `version` is still carried — into `built_by_version` at stamp
|
|
339
|
+
// time — because "which mikser built this" is worth knowing when
|
|
340
|
+
// reading a cache; it is simply not a reason to throw one away.
|
|
341
|
+
const fingerprint = cacheFingerprint(schemas)
|
|
295
342
|
|
|
296
343
|
// A config change invalidates the cache for the same reason a version
|
|
297
344
|
// change does: the derived state was computed under different rules.
|
|
@@ -323,13 +370,13 @@ export function createSqliteDatabase({
|
|
|
323
370
|
const reportOnly = isReportOnlyRun()
|
|
324
371
|
|
|
325
372
|
let upgradedFromVersion = null
|
|
326
|
-
if (reportOnly && !forceWipe && ((recorded && recorded !==
|
|
373
|
+
if (reportOnly && !forceWipe && ((recorded && recorded !== fingerprint) || configChanged)) {
|
|
327
374
|
logger?.warn(
|
|
328
375
|
'The cache is stale (%s changed since it was written) and this is a read-only run, '
|
|
329
376
|
+ 'so it was NOT wiped — the answer below describes the last build, which may not '
|
|
330
377
|
+ 'match your sources. Run a build to refresh it.',
|
|
331
378
|
configChanged ? 'config' : 'schema version')
|
|
332
|
-
} else if (configChanged && !(recorded && recorded !==
|
|
379
|
+
} else if (configChanged && !(recorded && recorded !== fingerprint)) {
|
|
333
380
|
logger?.warn(
|
|
334
381
|
'Config changed since the last run. Wiping the cache and rebuilding from sources '
|
|
335
382
|
+ '(files are the source of truth — no source data is affected). The stamp covers %s and '
|
|
@@ -337,7 +384,7 @@ export function createSqliteDatabase({
|
|
|
337
384
|
runtime.options.config,
|
|
338
385
|
)
|
|
339
386
|
}
|
|
340
|
-
if (!reportOnly && (forceWipe || (recorded && recorded !==
|
|
387
|
+
if (!reportOnly && (forceWipe || (recorded && recorded !== fingerprint) || configChanged)) {
|
|
341
388
|
// Schema mismatch on upgrade or downgrade. Per ADR-0002 the
|
|
342
389
|
// files on disk are the source of truth and this database
|
|
343
390
|
// is a derived cache, so the right behavior is to wipe the
|
|
@@ -348,10 +395,27 @@ export function createSqliteDatabase({
|
|
|
348
395
|
// expect a cold-start rebuild on this run. No data loss
|
|
349
396
|
// beyond the cache itself; everything in mikser.sqlite is
|
|
350
397
|
// recoverable from the working folder.
|
|
351
|
-
if (recorded && recorded !==
|
|
398
|
+
if (recorded && recorded !== fingerprint) {
|
|
399
|
+
// Names WHICH half moved, because the two mean different
|
|
400
|
+
// things to whoever is reading. A shape bump is a deliberate
|
|
401
|
+
// decision someone made in this release; a SQL change is a
|
|
402
|
+
// table that moved. "stored=10.0.1, current=10.1.0" said
|
|
403
|
+
// neither, and could not — it only ever meant "a release
|
|
404
|
+
// happened".
|
|
405
|
+
const [storedShape, storedSql] = String(recorded).split(':')
|
|
406
|
+
const [shape, sql] = fingerprint.split(':')
|
|
407
|
+
const cause = storedSql === undefined
|
|
408
|
+
? 'the cache predates schema fingerprints'
|
|
409
|
+
: storedShape !== shape && storedSql !== sql
|
|
410
|
+
? 'the schema tables AND the meaning of what is stored both changed'
|
|
411
|
+
: storedShape !== shape
|
|
412
|
+
? 'this release changed the meaning of what is already stored'
|
|
413
|
+
: 'the schema tables changed'
|
|
352
414
|
logger?.warn(
|
|
353
|
-
'
|
|
354
|
-
|
|
415
|
+
'Cache shape changed — %s (stored=%s, current=%s, this is mikser %s). Wiping the cache '
|
|
416
|
+
+ 'and rebuilding from sources (files are the source of truth — no source data is '
|
|
417
|
+
+ 'affected). A release that changes neither reuses the cache.',
|
|
418
|
+
cause, recorded, fingerprint, version,
|
|
355
419
|
)
|
|
356
420
|
} else if (forceWipe) {
|
|
357
421
|
logger?.info('Clearing the cache and rebuilding from sources.')
|
|
@@ -361,8 +425,8 @@ export function createSqliteDatabase({
|
|
|
361
425
|
// the same output whether the version moved, the config moved, or
|
|
362
426
|
// someone passed --clear.
|
|
363
427
|
reportWipe(
|
|
364
|
-
recorded && recorded !==
|
|
365
|
-
recorded && recorded !==
|
|
428
|
+
recorded && recorded !== fingerprint ? 'version' : forceWipe ? 'clear' : 'config',
|
|
429
|
+
recorded && recorded !== fingerprint ? { from: recorded, to: fingerprint } : {},
|
|
366
430
|
)
|
|
367
431
|
|
|
368
432
|
// Unlink, rather than dropping table by table.
|
|
@@ -391,9 +455,24 @@ export function createSqliteDatabase({
|
|
|
391
455
|
handle = new Database(dbPath)
|
|
392
456
|
setupConnection()
|
|
393
457
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
458
|
+
// Stamped when a cycle FINISHES, not when the database opens.
|
|
459
|
+
//
|
|
460
|
+
// The stamp is the only record of "this cache was rebuilt for this
|
|
461
|
+
// version", and writing it at open made it a record of "this cache
|
|
462
|
+
// was opened", which is a different and much weaker claim. A wipe
|
|
463
|
+
// followed by an interrupted rebuild left the version current and the
|
|
464
|
+
// cache half-built: the import had finished, so the catalog matched
|
|
465
|
+
// disk, but nothing had rendered, so there were no snapshots and the
|
|
466
|
+
// output folder still held the previous build. Every later start read
|
|
467
|
+
// a matching version, wiped nothing, correctly reported "N unchanged"
|
|
468
|
+
// and rendered nothing — a site frozen on the last good output, green
|
|
469
|
+
// on every signal, recoverable only by --force.
|
|
470
|
+
//
|
|
471
|
+
// Deferred, the same sequence self-heals: the interrupted run leaves
|
|
472
|
+
// no stamp, so the next start sees a mismatch and rebuilds properly.
|
|
473
|
+
// config_checksum goes with it for the same reason — a config change
|
|
474
|
+
// that half-applied must not look applied.
|
|
475
|
+
pendingStamp = { fingerprint, version, config: currentConfig }
|
|
397
476
|
|
|
398
477
|
// Build provisioning context. firstRun is true when the file
|
|
399
478
|
// didn't exist before this open OR when the schema mismatch
|
|
@@ -444,6 +523,43 @@ export function createSqliteDatabase({
|
|
|
444
523
|
}
|
|
445
524
|
}
|
|
446
525
|
|
|
526
|
+
// Did the last rebuild finish?
|
|
527
|
+
//
|
|
528
|
+
// Now that the stamp means "a cycle completed", its ABSENCE beside a
|
|
529
|
+
// populated catalog is a fact worth acting on: entities were imported
|
|
530
|
+
// and nothing ever finalized. The catalog matches disk, so every gate
|
|
531
|
+
// that reasons about inputs correctly says "unchanged"; the manifest
|
|
532
|
+
// is empty, so nothing has been rendered; and the output folder still
|
|
533
|
+
// holds whatever the last complete build wrote, because a cache wipe
|
|
534
|
+
// unlinks the database and nothing else.
|
|
535
|
+
//
|
|
536
|
+
// No layer can see this on its own. The source gate's evidence is
|
|
537
|
+
// sound, the dispatcher seeds from a journal that was discarded with
|
|
538
|
+
// the interrupted run, and the render gate — which would answer
|
|
539
|
+
// `never-rendered` — is never asked, because nothing dispatches to it.
|
|
540
|
+
// So it is declared as an override, and invalidation.js hands it to
|
|
541
|
+
// every gate at once.
|
|
542
|
+
//
|
|
543
|
+
// An empty catalog here is an ordinary first run or a completed wipe:
|
|
544
|
+
// nothing to reconcile, and emptiness already opens every gate.
|
|
545
|
+
if (!handle.prepare('SELECT value FROM mikser_meta WHERE key = ?').get('schema_version')) {
|
|
546
|
+
const table = handle.prepare(
|
|
547
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='mikser_entities'").get()
|
|
548
|
+
const entities = table
|
|
549
|
+
? handle.prepare('SELECT count(*) AS count FROM mikser_entities').get()?.count ?? 0
|
|
550
|
+
: 0
|
|
551
|
+
if (entities > 0) {
|
|
552
|
+
logger?.warn(
|
|
553
|
+
'The last rebuild did not finish — %d entities were imported and no cycle completed, '
|
|
554
|
+
+ 'so the catalog describes your sources while nothing has been rendered from them and '
|
|
555
|
+
+ 'the output folder still holds the previous build. Rebuilding everything on this run. '
|
|
556
|
+
+ '(A cache wipe followed by a restart mid-cycle does this; without the check the site '
|
|
557
|
+
+ 'stays on the old output and every build reports "unchanged".)',
|
|
558
|
+
entities,
|
|
559
|
+
)
|
|
560
|
+
runtime.options.cacheRebuildInterrupted = true
|
|
561
|
+
}
|
|
562
|
+
}
|
|
447
563
|
}
|
|
448
564
|
|
|
449
565
|
function close() {
|
|
@@ -467,6 +583,21 @@ export function createSqliteDatabase({
|
|
|
467
583
|
path: dbPath,
|
|
468
584
|
open,
|
|
469
585
|
close,
|
|
586
|
+
// Called once a cycle has finalized — see pendingStamp. Idempotent:
|
|
587
|
+
// the second cycle of a watch run has nothing left to write.
|
|
588
|
+
commitStamp() {
|
|
589
|
+
if (!pendingStamp || !handle) return false
|
|
590
|
+
const stamp = handle.prepare('INSERT OR REPLACE INTO mikser_meta (key, value) VALUES (?, ?)')
|
|
591
|
+
stamp.run('schema_version', pendingStamp.fingerprint)
|
|
592
|
+
// Which release wrote this cache. Recorded because it is the
|
|
593
|
+
// first thing a person wants when reading one, and deliberately
|
|
594
|
+
// NOT compared against anything — that was the bug.
|
|
595
|
+
stamp.run('built_by_version', pendingStamp.version)
|
|
596
|
+
if (pendingStamp.config) stamp.run('config_checksum', pendingStamp.config)
|
|
597
|
+
pendingStamp = null
|
|
598
|
+
runtime.options.cacheRebuildInterrupted = false
|
|
599
|
+
return true
|
|
600
|
+
},
|
|
470
601
|
get isOpen() { return handle !== null },
|
|
471
602
|
// Provisioning context from the most recent open. Plugins'
|
|
472
603
|
// onLoaded handlers can read this without subscribing to
|
|
@@ -492,6 +623,20 @@ export function createSqliteDatabase({
|
|
|
492
623
|
}
|
|
493
624
|
}
|
|
494
625
|
|
|
626
|
+
// The cache is stamped for this version only once a cycle has run to the
|
|
627
|
+
// end. Registered at module level rather than per-open so a watch server
|
|
628
|
+
// that re-opens nothing still stamps its first completed cycle, and so the
|
|
629
|
+
// hook cannot accumulate across opens.
|
|
630
|
+
//
|
|
631
|
+
// onFinalized, not onFinalize: the manifest writes this cycle's snapshots in
|
|
632
|
+
// onFinalize, and the stamp claims that work happened. Claiming it one hook
|
|
633
|
+
// early would reintroduce the failure in miniature.
|
|
634
|
+
onFinalized(async () => {
|
|
635
|
+
if (db?.commitStamp?.()) {
|
|
636
|
+
useLogger()?.debug('Cache stamped — a full cycle completed for this schema version')
|
|
637
|
+
}
|
|
638
|
+
})
|
|
639
|
+
|
|
495
640
|
onLoaded(async () => {
|
|
496
641
|
if (db?.isOpen) return // multi-cycle watch mode — keep the open connection
|
|
497
642
|
|
package/src/engine.js
CHANGED
|
@@ -1575,10 +1575,34 @@ The full version, with what each code means: docs/diagnostics.md`)
|
|
|
1575
1575
|
// undefined here. The logger has no such problem — it writes during the
|
|
1576
1576
|
// run, by which time options exist.
|
|
1577
1577
|
const quietStdout = ['--json', '--tool', '--tools'].some(flag => process.argv.includes(flag))
|
|
1578
|
+
// Through the logger when nobody is watching, so it gets a timestamp.
|
|
1579
|
+
//
|
|
1580
|
+
// This line marks a process start, which in a supervisor's log is the
|
|
1581
|
+
// most useful thing on the page — it is how a restart is found at all.
|
|
1582
|
+
// Written straight to the stream it was undated and, worse, carried its
|
|
1583
|
+
// own hardcoded escapes into a file that no terminal would ever render.
|
|
1584
|
+
//
|
|
1585
|
+
// The decorated form stays for a terminal, where it is a banner rather
|
|
1586
|
+
// than a record.
|
|
1578
1587
|
if (runtime.options?.json || quietStdout) {
|
|
1588
|
+
// Written straight to stderr, NOT through the logger: the logger picks
|
|
1589
|
+
// its sink per-write from runtime.options.json, which commander has
|
|
1590
|
+
// not parsed yet — the same reason quietStdout reads argv above. A
|
|
1591
|
+
// banner routed through it here lands on stdout and turns the
|
|
1592
|
+
// document into a parse error, which is the failure this branch was
|
|
1593
|
+
// added to prevent in the first place.
|
|
1579
1594
|
process.stderr.write(`mikser. ${packageInfo.version}\n`)
|
|
1580
|
-
} else {
|
|
1595
|
+
} else if (process.stdout.isTTY && !process.env.NO_COLOR) {
|
|
1581
1596
|
console.info('\x1b[1mmikser\x1b[22;5;38;2;255;63;0m.\x1b[0m %s\n', packageInfo.version)
|
|
1597
|
+
} else {
|
|
1598
|
+
// Redirected: through the logger, so the line that marks a process
|
|
1599
|
+
// start carries a timestamp. In a supervisor's log that is the most
|
|
1600
|
+
// useful line on the page — it is how a restart is found at all — and
|
|
1601
|
+
// written straight to the stream it was undated and carried its own
|
|
1602
|
+
// escapes into a file no terminal will render.
|
|
1603
|
+
const logger = useLogger()
|
|
1604
|
+
if (logger) logger.info('mikser. %s', packageInfo.version)
|
|
1605
|
+
else process.stdout.write(`mikser. ${packageInfo.version}\n`)
|
|
1582
1606
|
}
|
|
1583
1607
|
return runtime
|
|
1584
1608
|
}
|
package/src/invalidation.js
CHANGED
|
@@ -50,6 +50,7 @@ export const REASON = Object.freeze({
|
|
|
50
50
|
FORCE: 'force',
|
|
51
51
|
CACHE_INVALIDATED: 'cache-invalidated',
|
|
52
52
|
RELOAD: 'reload',
|
|
53
|
+
REBUILD_INTERRUPTED: 'rebuild-interrupted',
|
|
53
54
|
OUTPUT_MISSING: 'output-missing',
|
|
54
55
|
// Evidence — each layer's business, named here so the vocabulary is
|
|
55
56
|
// legible as a whole.
|
|
@@ -130,6 +131,12 @@ export function bypassReason({ reload = false, id } = {}) {
|
|
|
130
131
|
if (reload) return REASON.RELOAD
|
|
131
132
|
if (runtime.options?.force) return REASON.FORCE
|
|
132
133
|
if (runtime.catalog?.cacheInvalidated) return REASON.CACHE_INVALIDATED
|
|
134
|
+
// The previous rebuild imported entities and never finalized, so the
|
|
135
|
+
// catalog describes sources that nothing has rendered. Set by
|
|
136
|
+
// database/index.js at open, cleared when a cycle stamps the cache.
|
|
137
|
+
// Declared here so it reaches every gate — which is the whole reason
|
|
138
|
+
// this module exists, and this is the first override added since.
|
|
139
|
+
if (runtime.options?.cacheRebuildInterrupted) return REASON.REBUILD_INTERRUPTED
|
|
133
140
|
if (id !== undefined && missingOutputIds().has(id)) return REASON.OUTPUT_MISSING
|
|
134
141
|
return null
|
|
135
142
|
}
|
package/src/logger.js
CHANGED
|
@@ -154,16 +154,44 @@ function createTerminalStream() {
|
|
|
154
154
|
// trace} resolves to, defaulting to 'info'.
|
|
155
155
|
export function createMikserLogger(level = 'info') {
|
|
156
156
|
const terminalStream = createTerminalStream()
|
|
157
|
+
|
|
158
|
+
// A terminal is watched as it happens. A file is read afterwards.
|
|
159
|
+
//
|
|
160
|
+
// The minimal format below is right for the first and wrong for the
|
|
161
|
+
// second, and until now it was used for both — so a supervisor's log was
|
|
162
|
+
// a wall of undated lines wearing ANSI escapes. Reconstructing an
|
|
163
|
+
// incident from one meant ordering events by file mtimes and git commit
|
|
164
|
+
// dates because the build's own log could not say when anything happened,
|
|
165
|
+
// and every excerpt had to be piped through sed to be readable.
|
|
166
|
+
//
|
|
167
|
+
// Decided from the stream these lines actually land on, which is stderr
|
|
168
|
+
// under --json / --tool (stdout carries the document there). The logger is
|
|
169
|
+
// rebuilt at onLoad, by which point those options are parsed, so the
|
|
170
|
+
// second construction gets it right even if the first cannot.
|
|
171
|
+
//
|
|
172
|
+
// Same signal the progress bar already uses — a gauge is pointless in a
|
|
173
|
+
// file for the same reason a timestamp is pointless on a terminal.
|
|
174
|
+
const target = (runtime.options?.json || runtime.options?.tool || runtime.options?.tools)
|
|
175
|
+
? process.stderr : process.stdout
|
|
176
|
+
const attended = Boolean(target.isTTY)
|
|
177
|
+
|
|
157
178
|
const prettyStream = pretty({
|
|
158
179
|
destination: terminalStream,
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
|
|
180
|
+
// NO_COLOR is honoured on a terminal too; nobody wants escapes in a
|
|
181
|
+
// file regardless.
|
|
182
|
+
colorize: attended && !process.env.NO_COLOR,
|
|
183
|
+
// `SYS:standard` carries the date, milliseconds AND the UTC offset.
|
|
184
|
+
// The offset is not decoration: the incident that prompted this
|
|
185
|
+
// needed a container's clock lined up against commit dates in
|
|
186
|
+
// another zone, and a bare wall-clock time cannot answer that.
|
|
187
|
+
...(attended ? {} : { translateTime: 'SYS:standard' }),
|
|
188
|
+
// apt-like minimal format: hide pid / hostname, and suppress the
|
|
189
|
+
// level prefix entirely via a customPrettifier that returns an empty
|
|
190
|
+
// string. The icon prepended by messageFormat (🟡 / 🔴 / 🟢 / …) is
|
|
191
|
+
// what signals level. The raw pino record still carries `level`, so
|
|
192
|
+
// third-party transports get full structured data. `time` is dropped
|
|
193
|
+
// only when someone is watching.
|
|
194
|
+
ignore: attended ? 'pid,hostname,time' : 'pid,hostname',
|
|
167
195
|
// The terminal gets the SENTENCE; the structured fields go to the
|
|
168
196
|
// report and to transports.
|
|
169
197
|
//
|