mikser-io 10.1.0 → 10.2.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 +85 -4
- 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
|
@@ -48,7 +48,7 @@ import Database from 'better-sqlite3'
|
|
|
48
48
|
import runtime from '../runtime.js'
|
|
49
49
|
import { isReportOnlyRun } from '../tools.js'
|
|
50
50
|
import { reportWipe } from '../report.js'
|
|
51
|
-
import { onLoaded } from '../lifecycle.js'
|
|
51
|
+
import { onLoaded, onFinalized } from '../lifecycle.js'
|
|
52
52
|
import packageInfo from '../../package.json' with { type: 'json' }
|
|
53
53
|
|
|
54
54
|
// Local logger resolver — same one-liner engine.js exports as
|
|
@@ -242,6 +242,10 @@ export function createSqliteDatabase({
|
|
|
242
242
|
// `--clear` asks for. Removes the file; nothing durable is in it.
|
|
243
243
|
forceWipe = false,
|
|
244
244
|
}) {
|
|
245
|
+
// Set at open, written at the first successful finalize. See the note
|
|
246
|
+
// where it is assigned.
|
|
247
|
+
let pendingStamp = null
|
|
248
|
+
|
|
245
249
|
// Tests inject their own provisioners; the runtime path falls
|
|
246
250
|
// through to the module-level `provisioners` array that plugins
|
|
247
251
|
// populate via onProvision() at module-eval.
|
|
@@ -391,9 +395,24 @@ export function createSqliteDatabase({
|
|
|
391
395
|
handle = new Database(dbPath)
|
|
392
396
|
setupConnection()
|
|
393
397
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
398
|
+
// Stamped when a cycle FINISHES, not when the database opens.
|
|
399
|
+
//
|
|
400
|
+
// The stamp is the only record of "this cache was rebuilt for this
|
|
401
|
+
// version", and writing it at open made it a record of "this cache
|
|
402
|
+
// was opened", which is a different and much weaker claim. A wipe
|
|
403
|
+
// followed by an interrupted rebuild left the version current and the
|
|
404
|
+
// cache half-built: the import had finished, so the catalog matched
|
|
405
|
+
// disk, but nothing had rendered, so there were no snapshots and the
|
|
406
|
+
// output folder still held the previous build. Every later start read
|
|
407
|
+
// a matching version, wiped nothing, correctly reported "N unchanged"
|
|
408
|
+
// and rendered nothing — a site frozen on the last good output, green
|
|
409
|
+
// on every signal, recoverable only by --force.
|
|
410
|
+
//
|
|
411
|
+
// Deferred, the same sequence self-heals: the interrupted run leaves
|
|
412
|
+
// no stamp, so the next start sees a mismatch and rebuilds properly.
|
|
413
|
+
// config_checksum goes with it for the same reason — a config change
|
|
414
|
+
// that half-applied must not look applied.
|
|
415
|
+
pendingStamp = { version, config: currentConfig }
|
|
397
416
|
|
|
398
417
|
// Build provisioning context. firstRun is true when the file
|
|
399
418
|
// didn't exist before this open OR when the schema mismatch
|
|
@@ -444,6 +463,43 @@ export function createSqliteDatabase({
|
|
|
444
463
|
}
|
|
445
464
|
}
|
|
446
465
|
|
|
466
|
+
// Did the last rebuild finish?
|
|
467
|
+
//
|
|
468
|
+
// Now that the stamp means "a cycle completed", its ABSENCE beside a
|
|
469
|
+
// populated catalog is a fact worth acting on: entities were imported
|
|
470
|
+
// and nothing ever finalized. The catalog matches disk, so every gate
|
|
471
|
+
// that reasons about inputs correctly says "unchanged"; the manifest
|
|
472
|
+
// is empty, so nothing has been rendered; and the output folder still
|
|
473
|
+
// holds whatever the last complete build wrote, because a cache wipe
|
|
474
|
+
// unlinks the database and nothing else.
|
|
475
|
+
//
|
|
476
|
+
// No layer can see this on its own. The source gate's evidence is
|
|
477
|
+
// sound, the dispatcher seeds from a journal that was discarded with
|
|
478
|
+
// the interrupted run, and the render gate — which would answer
|
|
479
|
+
// `never-rendered` — is never asked, because nothing dispatches to it.
|
|
480
|
+
// So it is declared as an override, and invalidation.js hands it to
|
|
481
|
+
// every gate at once.
|
|
482
|
+
//
|
|
483
|
+
// An empty catalog here is an ordinary first run or a completed wipe:
|
|
484
|
+
// nothing to reconcile, and emptiness already opens every gate.
|
|
485
|
+
if (!handle.prepare('SELECT value FROM mikser_meta WHERE key = ?').get('schema_version')) {
|
|
486
|
+
const table = handle.prepare(
|
|
487
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='mikser_entities'").get()
|
|
488
|
+
const entities = table
|
|
489
|
+
? handle.prepare('SELECT count(*) AS count FROM mikser_entities').get()?.count ?? 0
|
|
490
|
+
: 0
|
|
491
|
+
if (entities > 0) {
|
|
492
|
+
logger?.warn(
|
|
493
|
+
'The last rebuild did not finish — %d entities were imported and no cycle completed, '
|
|
494
|
+
+ 'so the catalog describes your sources while nothing has been rendered from them and '
|
|
495
|
+
+ 'the output folder still holds the previous build. Rebuilding everything on this run. '
|
|
496
|
+
+ '(A cache wipe followed by a restart mid-cycle does this; without the check the site '
|
|
497
|
+
+ 'stays on the old output and every build reports "unchanged".)',
|
|
498
|
+
entities,
|
|
499
|
+
)
|
|
500
|
+
runtime.options.cacheRebuildInterrupted = true
|
|
501
|
+
}
|
|
502
|
+
}
|
|
447
503
|
}
|
|
448
504
|
|
|
449
505
|
function close() {
|
|
@@ -467,6 +523,17 @@ export function createSqliteDatabase({
|
|
|
467
523
|
path: dbPath,
|
|
468
524
|
open,
|
|
469
525
|
close,
|
|
526
|
+
// Called once a cycle has finalized — see pendingStamp. Idempotent:
|
|
527
|
+
// the second cycle of a watch run has nothing left to write.
|
|
528
|
+
commitStamp() {
|
|
529
|
+
if (!pendingStamp || !handle) return false
|
|
530
|
+
const stamp = handle.prepare('INSERT OR REPLACE INTO mikser_meta (key, value) VALUES (?, ?)')
|
|
531
|
+
stamp.run('schema_version', pendingStamp.version)
|
|
532
|
+
if (pendingStamp.config) stamp.run('config_checksum', pendingStamp.config)
|
|
533
|
+
pendingStamp = null
|
|
534
|
+
runtime.options.cacheRebuildInterrupted = false
|
|
535
|
+
return true
|
|
536
|
+
},
|
|
470
537
|
get isOpen() { return handle !== null },
|
|
471
538
|
// Provisioning context from the most recent open. Plugins'
|
|
472
539
|
// onLoaded handlers can read this without subscribing to
|
|
@@ -492,6 +559,20 @@ export function createSqliteDatabase({
|
|
|
492
559
|
}
|
|
493
560
|
}
|
|
494
561
|
|
|
562
|
+
// The cache is stamped for this version only once a cycle has run to the
|
|
563
|
+
// end. Registered at module level rather than per-open so a watch server
|
|
564
|
+
// that re-opens nothing still stamps its first completed cycle, and so the
|
|
565
|
+
// hook cannot accumulate across opens.
|
|
566
|
+
//
|
|
567
|
+
// onFinalized, not onFinalize: the manifest writes this cycle's snapshots in
|
|
568
|
+
// onFinalize, and the stamp claims that work happened. Claiming it one hook
|
|
569
|
+
// early would reintroduce the failure in miniature.
|
|
570
|
+
onFinalized(async () => {
|
|
571
|
+
if (db?.commitStamp?.()) {
|
|
572
|
+
useLogger()?.debug('Cache stamped — a full cycle completed for this schema version')
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
|
|
495
576
|
onLoaded(async () => {
|
|
496
577
|
if (db?.isOpen) return // multi-cycle watch mode — keep the open connection
|
|
497
578
|
|
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
|
//
|