mikser-io 11.10.3 → 11.10.5
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/invalidation.js +53 -0
- package/src/manifest/cycle.js +41 -2
- package/src/manifest/index.js +47 -0
package/package.json
CHANGED
package/src/invalidation.js
CHANGED
|
@@ -52,6 +52,12 @@ export const REASON = Object.freeze({
|
|
|
52
52
|
RELOAD: 'reload',
|
|
53
53
|
REBUILD_INTERRUPTED: 'rebuild-interrupted',
|
|
54
54
|
OUTPUT_MISSING: 'output-missing',
|
|
55
|
+
// A catalog entity with a layout that has no snapshot at all — distinct
|
|
56
|
+
// from NEVER_RENDERED below, which is a gate reporting what it found. This
|
|
57
|
+
// one is an OVERRIDE: nothing would otherwise look at such an entity,
|
|
58
|
+
// because every other mechanism starts from a snapshot. Claimed once per
|
|
59
|
+
// entity per process; see claimUnrendered.
|
|
60
|
+
NEVER_RECORDED: 'never-recorded',
|
|
55
61
|
// Evidence — each layer's business, named here so the vocabulary is
|
|
56
62
|
// legible as a whole.
|
|
57
63
|
UNCHANGED: 'unchanged',
|
|
@@ -117,6 +123,49 @@ export function missingOutputIds() {
|
|
|
117
123
|
|
|
118
124
|
export function forgetMissingOutputs() {
|
|
119
125
|
cachedMissingOutputs = null
|
|
126
|
+
// `unrenderedOnce` is NOT dropped here. It is per PROCESS, not per cycle:
|
|
127
|
+
// clearing it would recompute the set every cycle and hand back the same
|
|
128
|
+
// un-renderable entity every time, which is the churn the claim-once rule
|
|
129
|
+
// exists to prevent.
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Entities with a layout that have never recorded a snapshot.
|
|
133
|
+
//
|
|
134
|
+
// The gap `missingOutputIds` cannot see: it walks SNAPSHOTS, so an entity
|
|
135
|
+
// that never got one is invisible to it, and nothing else looks either — the
|
|
136
|
+
// source gate sees the file unchanged and never journals it again. That is
|
|
137
|
+
// the state a superseded DELETE left behind, and the reason it was permanent:
|
|
138
|
+
// a missing page, green builds forever, and only --audit-output to say so.
|
|
139
|
+
//
|
|
140
|
+
// ONE DISPATCH PER ENTITY, per process, which is the whole safety of it.
|
|
141
|
+
// Membership is CLAIMED rather than read: an id comes out once and is then
|
|
142
|
+
// gone from the set. An entity that legitimately cannot render — a layout
|
|
143
|
+
// that produces no destination is the real case, and `mikser_explain` names
|
|
144
|
+
// it — therefore costs one wasted dispatch rather than one per cycle for the
|
|
145
|
+
// life of the process. Re-dispatching such an entity forever is the failure
|
|
146
|
+
// this net could otherwise become, and it would be quiet: slower builds and
|
|
147
|
+
// nothing to point at.
|
|
148
|
+
//
|
|
149
|
+
// Computed lazily and only once, then dropped alongside the missing-output
|
|
150
|
+
// cache at the end of onFinalize.
|
|
151
|
+
let unrenderedOnce = null
|
|
152
|
+
|
|
153
|
+
export function claimUnrendered(id) {
|
|
154
|
+
if (id === undefined) return false
|
|
155
|
+
if (!unrenderedOnce) {
|
|
156
|
+
// Nothing has ever rendered, so nothing is missing: this is a cold
|
|
157
|
+
// build and every entity is about to be dispatched on its own merits.
|
|
158
|
+
// Without this the whole corpus answers `never-recorded` on a first
|
|
159
|
+
// build — true, useless, and it would take the real reason off every
|
|
160
|
+
// page in `--json`.
|
|
161
|
+
const recorded = runtime.manifest?.size?.() ?? 0
|
|
162
|
+
unrenderedOnce = recorded > 0
|
|
163
|
+
? new Set(runtime.manifest?.unrenderedIds?.() ?? [])
|
|
164
|
+
: new Set()
|
|
165
|
+
}
|
|
166
|
+
if (!unrenderedOnce.has(id)) return false
|
|
167
|
+
unrenderedOnce.delete(id)
|
|
168
|
+
return true
|
|
120
169
|
}
|
|
121
170
|
|
|
122
171
|
// What overrides a gate's own evidence, or null when nothing does.
|
|
@@ -138,6 +187,10 @@ export function bypassReason({ reload = false, id } = {}) {
|
|
|
138
187
|
// this module exists, and this is the first override added since.
|
|
139
188
|
if (runtime.options?.cacheRebuildInterrupted) return REASON.REBUILD_INTERRUPTED
|
|
140
189
|
if (id !== undefined && missingOutputIds().has(id)) return REASON.OUTPUT_MISSING
|
|
190
|
+
// Last, and after OUTPUT_MISSING on purpose: an entity with a snapshot
|
|
191
|
+
// whose file went missing is the better-understood case and keeps its own
|
|
192
|
+
// reason, which `--json` callers and the scenario suite already match on.
|
|
193
|
+
if (claimUnrendered(id)) return REASON.NEVER_RECORDED
|
|
141
194
|
return null
|
|
142
195
|
}
|
|
143
196
|
|
package/src/manifest/cycle.js
CHANGED
|
@@ -53,8 +53,47 @@ onFinalize(async () => {
|
|
|
53
53
|
const newDestinationsByParent = new Map()
|
|
54
54
|
const lostPagination = new Set()
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
// A DELETE only counts if nothing re-created the entity after it.
|
|
57
|
+
//
|
|
58
|
+
// Collected with no regard for supersession, this unlinked a page that
|
|
59
|
+
// had just been written. A file created, deleted and re-created AT THE
|
|
60
|
+
// SAME PATH while one cycle was held open leaves that cycle holding
|
|
61
|
+
// CREATE, DELETE and CREATE for one id — and since 11.10.2 a cancelled
|
|
62
|
+
// cycle's entries are carried rather than dropped, which is what lets
|
|
63
|
+
// the stale DELETE reach the cycle that finally renders the entity. That
|
|
64
|
+
// fix is right; this is the other end of it.
|
|
65
|
+
//
|
|
66
|
+
// The staging below and the `claimedByThisCycle` guard were both written
|
|
67
|
+
// for the RENAME case, where the DELETE carries the OLD id and the RENDER
|
|
68
|
+
// a NEW one. There the new destination is correctly protected. When the
|
|
69
|
+
// id is the SAME, the guard skipped the very destination just written,
|
|
70
|
+
// `stillClaimed` was empty because a first render has no snapshot row
|
|
71
|
+
// yet, and the unlink ran: an empty output directory, no snapshot, no
|
|
72
|
+
// failure, `Rendered: 1` and a green build. Nothing recovered it — the
|
|
73
|
+
// source gate saw the file unchanged, and missingOutputIds() iterates
|
|
74
|
+
// SNAPSHOTS, so an entity that never got one is invisible to it.
|
|
75
|
+
//
|
|
76
|
+
// ORDER, not catalog presence. Catalog presence gives the same answers
|
|
77
|
+
// today, but only because catalog.js's finalize drain happens to be
|
|
78
|
+
// registered before this one (index.js exports catalog.js first), so the
|
|
79
|
+
// row is already gone when cleanup reads it. That is an invisible
|
|
80
|
+
// coupling: reordering two exports would silently restore this bug. The
|
|
81
|
+
// journal knows the answer on its own, and mikser-io-layouts already
|
|
82
|
+
// reconciles the same journal by order when it seeds dispatch.
|
|
83
|
+
const lastDelete = new Map()
|
|
84
|
+
const lastWrite = new Map()
|
|
85
|
+
for await (const { id: seq, operation, entity } of useJournal(
|
|
86
|
+
'Manifest cleanup', [OPERATION.DELETE, OPERATION.CREATE, OPERATION.UPDATE])) {
|
|
87
|
+
if (!entity?.id) continue
|
|
88
|
+
const seen = operation === OPERATION.DELETE ? lastDelete : lastWrite
|
|
89
|
+
const prior = seen.get(entity.id)
|
|
90
|
+
if (prior === undefined || seq > prior) seen.set(entity.id, seq)
|
|
91
|
+
}
|
|
92
|
+
for (const [id, seq] of lastDelete) {
|
|
93
|
+
// `>` and not `>=`: a render that opts out of the catalog journals its
|
|
94
|
+
// DELETE after the entity's own CREATE, so the DELETE is genuinely
|
|
95
|
+
// later and must win — the rule 2e documents.
|
|
96
|
+
if (seq > (lastWrite.get(id) ?? -1)) deletedIds.push(id)
|
|
58
97
|
}
|
|
59
98
|
|
|
60
99
|
// Every destination claimed by a render task this cycle — whether it
|
package/src/manifest/index.js
CHANGED
|
@@ -65,6 +65,10 @@ import { sha1, describeInputChange, buildRefClosure, buildSnapshot, rowToSnap, s
|
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
export function createManifest(db) {
|
|
68
|
+
// See unrenderedIds(): prepared on first use, because it is the one
|
|
69
|
+
// statement that reaches outside the manifest's own tables.
|
|
70
|
+
let stmtUnrendered
|
|
71
|
+
|
|
68
72
|
const {
|
|
69
73
|
stmtCollisions,
|
|
70
74
|
stmtClaimants,
|
|
@@ -504,6 +508,49 @@ export function createManifest(db) {
|
|
|
504
508
|
return [...new Set(stmtAllFailures.all().map(row => row.id))]
|
|
505
509
|
},
|
|
506
510
|
|
|
511
|
+
// Catalog entities that should have rendered and never did.
|
|
512
|
+
//
|
|
513
|
+
// The state a superseded DELETE used to leave behind: a catalog row
|
|
514
|
+
// with a layout, an output written and then unlinked, and NO snapshot
|
|
515
|
+
// — which made it invisible to missingOutputIds(), because that walks
|
|
516
|
+
// snapshots. The source gate saw the file unchanged, so nothing
|
|
517
|
+
// journalled it again either. Permanently missing page, green builds
|
|
518
|
+
// forever, and only --audit-output to say so.
|
|
519
|
+
//
|
|
520
|
+
// Both exclusions are load-bearing:
|
|
521
|
+
// - a snapshot means it HAS rendered, so there is nothing to
|
|
522
|
+
// recover;
|
|
523
|
+
// - a failure means the render was attempted and threw. A failed
|
|
524
|
+
// render writes no snapshot ON PURPOSE, so the last good bytes
|
|
525
|
+
// survive, and the retry path owns that case. Re-dispatching it
|
|
526
|
+
// here would fight the mechanism that exists for it.
|
|
527
|
+
//
|
|
528
|
+
// `meta_layout` is the column the layouts plugin mirrors its match
|
|
529
|
+
// into and carries a partial index, so this is an index scan that
|
|
530
|
+
// finds nothing in a healthy build.
|
|
531
|
+
//
|
|
532
|
+
// PREPARED LAZILY, and that is not a style choice: this is the only
|
|
533
|
+
// statement here that reaches outside the manifest's own tables, and
|
|
534
|
+
// a manifest is legitimately built against a database that holds no
|
|
535
|
+
// catalog — the unit suite does exactly that. Preparing it eagerly
|
|
536
|
+
// threw SQLITE_ERROR at construction and took twenty-one unrelated
|
|
537
|
+
// tests with it.
|
|
538
|
+
unrenderedIds() {
|
|
539
|
+
if (stmtUnrendered === undefined) {
|
|
540
|
+
try {
|
|
541
|
+
stmtUnrendered = db.prepare(`
|
|
542
|
+
SELECT e.id FROM mikser_entities e
|
|
543
|
+
LEFT JOIN mikser_snapshots s ON s.id = e.id
|
|
544
|
+
LEFT JOIN mikser_failures f ON f.id = e.id
|
|
545
|
+
WHERE e.meta_layout IS NOT NULL AND s.id IS NULL AND f.id IS NULL
|
|
546
|
+
`)
|
|
547
|
+
} catch {
|
|
548
|
+
stmtUnrendered = null // no catalog here; nothing to recover
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return stmtUnrendered ? stmtUnrendered.all().map(row => row.id) : []
|
|
552
|
+
},
|
|
553
|
+
|
|
507
554
|
allFailures() {
|
|
508
555
|
return stmtAllFailures.all()
|
|
509
556
|
},
|