mikser-io 10.0.4 → 10.1.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/index.js +1 -0
- package/package.json +1 -1
- package/src/explain.js +3 -5
- package/src/invalidation.js +158 -0
- package/src/manifest.js +18 -75
- package/src/plugins/assets.js +6 -3
- package/src/plugins/files.js +3 -6
- package/src/source.js +9 -22
- package/src/utils.js +0 -23
package/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { default as runtime } from './src/runtime.js'
|
|
2
2
|
export * as constants from './src/constants.js'
|
|
3
3
|
export * from './src/utils.js'
|
|
4
|
+
export * from './src/invalidation.js'
|
|
4
5
|
export * from './src/auth.js'
|
|
5
6
|
export * from './src/roles.js'
|
|
6
7
|
export * from './src/inventory.js'
|
package/package.json
CHANGED
package/src/explain.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// Follows --audit-output's shape: report and exit, no build phases run.
|
|
11
11
|
import { existsSync } from 'node:fs'
|
|
12
12
|
import { inputHashOf, inputPartsOf, diffInputParts, lookupKeys, checksum as fileChecksum } from './utils.js'
|
|
13
|
-
import {
|
|
13
|
+
import { outputMissing } from './invalidation.js'
|
|
14
14
|
import { filterKey } from './track.js'
|
|
15
15
|
import { findEntity, findEntities, findById } from './catalog.js'
|
|
16
16
|
import runtime from './runtime.js'
|
|
@@ -141,7 +141,7 @@ export async function explain(reference) {
|
|
|
141
141
|
// attempt — if the last render threw, that is the more useful sentence,
|
|
142
142
|
// and it explains the absence too.
|
|
143
143
|
const missingDestinations = snapshots
|
|
144
|
-
.filter(snap =>
|
|
144
|
+
.filter(snap => outputMissing(snap.destination))
|
|
145
145
|
.map(snap => snap.destination)
|
|
146
146
|
|
|
147
147
|
// The catalog is as of the LAST BUILD. If the file has been edited since,
|
|
@@ -244,9 +244,7 @@ export async function explain(reference) {
|
|
|
244
244
|
// re-renders. --explain contradicting the build is worse than
|
|
245
245
|
// --explain being incomplete, because it is the tool someone
|
|
246
246
|
// reaches for when the build has already surprised them.
|
|
247
|
-
missing: snap.destination
|
|
248
|
-
? !existsSync(resolveOutputPath(snap.destination))
|
|
249
|
-
: false,
|
|
247
|
+
missing: outputMissing(snap.destination),
|
|
250
248
|
outputHash: snap.outputHash ?? null,
|
|
251
249
|
parent: snap.parent ?? null,
|
|
252
250
|
// Which keys of its OWN meta the render read, and which keys it
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Should this work be redone?
|
|
2
|
+
//
|
|
3
|
+
// One question, asked at four layers with different evidence in hand:
|
|
4
|
+
//
|
|
5
|
+
// source.js a file's checksum against the catalog's
|
|
6
|
+
// manifest a recorded hash, for the layouts dispatcher's seeding
|
|
7
|
+
// manifest a snapshot plus its refClosure, at the render gate
|
|
8
|
+
// assets a `.md5` marker at the current preset revision
|
|
9
|
+
//
|
|
10
|
+
// The evidence differs and belongs to each layer. What does NOT differ is
|
|
11
|
+
// what OVERRIDES the evidence — and that is what this module owns, because
|
|
12
|
+
// it was written four times and drifted the moment a fifth override was
|
|
13
|
+
// added to one of them.
|
|
14
|
+
//
|
|
15
|
+
// The drift, concretely: `rm -rf out` changes no input, so every layer
|
|
16
|
+
// answered "unchanged" and a build rendered nothing, printed no `Rendered:`
|
|
17
|
+
// line and exited 0 over an empty output folder. Adding an output-existence
|
|
18
|
+
// check to the render gate fixed nothing, because the source gate had
|
|
19
|
+
// already dropped the entity; adding it there fixed the site but not image
|
|
20
|
+
// derivatives, because files.js held a private copy of that gate; and fixing
|
|
21
|
+
// that still left the assets marker, which had never asked about the file at
|
|
22
|
+
// all. Five gates, one question, four implementations of the override rules.
|
|
23
|
+
//
|
|
24
|
+
// So the rule is: a layer may own what it KNOWS. It may not own what
|
|
25
|
+
// overrides it. Anything that makes a gate not apply — --force, a wiped
|
|
26
|
+
// cache, a reload event, an output that is gone — is declared here once, and
|
|
27
|
+
// a new one added here reaches every gate that ever asks.
|
|
28
|
+
//
|
|
29
|
+
// Deliberately NOT moved here: the dependency-graph walk in
|
|
30
|
+
// manifest.skipDecision. That reasons over snapshots, refClosures and sift
|
|
31
|
+
// filters the manifest owns, and hauling it out would trade a real coupling
|
|
32
|
+
// for a fake one. It asks this module what overrides its answer, which is the
|
|
33
|
+
// part that was duplicated.
|
|
34
|
+
//
|
|
35
|
+
// A leaf module: runtime and node builtins only. It reads `runtime.manifest`
|
|
36
|
+
// lazily rather than importing it, the way every other consumer of engine
|
|
37
|
+
// state does, so the manifest can import this without a cycle.
|
|
38
|
+
|
|
39
|
+
import path from 'node:path'
|
|
40
|
+
import { existsSync } from 'node:fs'
|
|
41
|
+
import runtime from './runtime.js'
|
|
42
|
+
|
|
43
|
+
// The vocabulary, in one place because it is asserted against.
|
|
44
|
+
//
|
|
45
|
+
// `--json` carries these strings out to callers and the scenario suite
|
|
46
|
+
// matches on them, so they are API. Defined here rather than in the gate
|
|
47
|
+
// that happens to emit each one, so the set can be read as a set.
|
|
48
|
+
export const REASON = Object.freeze({
|
|
49
|
+
// Overrides — this module's business.
|
|
50
|
+
FORCE: 'force',
|
|
51
|
+
CACHE_INVALIDATED: 'cache-invalidated',
|
|
52
|
+
RELOAD: 'reload',
|
|
53
|
+
OUTPUT_MISSING: 'output-missing',
|
|
54
|
+
// Evidence — each layer's business, named here so the vocabulary is
|
|
55
|
+
// legible as a whole.
|
|
56
|
+
UNCHANGED: 'unchanged',
|
|
57
|
+
NEVER_RENDERED: 'never-rendered',
|
|
58
|
+
INPUTS_CHANGED: 'inputs-changed',
|
|
59
|
+
REF_CHANGED: 'ref-changed',
|
|
60
|
+
QUERY_MATCHED: 'query-matched',
|
|
61
|
+
CACHE_DISABLED: 'cache-disabled',
|
|
62
|
+
RETRY_FAILED: 'retry-failed',
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// Where an output lives, given what a snapshot recorded.
|
|
66
|
+
//
|
|
67
|
+
// Destinations come in two shapes and both are legitimate: output-relative
|
|
68
|
+
// for a rendered page (`/index.html`), absolute for a derivative the assets
|
|
69
|
+
// plugin places itself. Resolving relative-first and falling back to the
|
|
70
|
+
// literal path covers both without the caller having to know which it holds.
|
|
71
|
+
//
|
|
72
|
+
// Returns the joined path even when nothing exists there, so a caller can
|
|
73
|
+
// report WHICH path it looked for — "missing" without a path is a fact
|
|
74
|
+
// nobody can act on.
|
|
75
|
+
export function resolveOutputPath(destination, outputFolder = runtime.options?.outputFolder) {
|
|
76
|
+
if (!destination) return undefined
|
|
77
|
+
const joined = path.join(outputFolder ?? '', destination)
|
|
78
|
+
if (existsSync(joined)) return joined
|
|
79
|
+
if (path.isAbsolute(destination) && existsSync(destination)) return destination
|
|
80
|
+
return joined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Is the file this destination names gone?
|
|
84
|
+
export function outputMissing(destination) {
|
|
85
|
+
if (!destination) return false
|
|
86
|
+
return !existsSync(resolveOutputPath(destination))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Memoized for the cycle.
|
|
90
|
+
//
|
|
91
|
+
// Every gate asks, and they all ask before anything has rendered, so one walk
|
|
92
|
+
// answers all of them. Dropped at the end of onFinalize, where this cycle's
|
|
93
|
+
// renders are recorded and the answer stops being true — not at the start of
|
|
94
|
+
// the next cycle, because in watch mode there may not be one for hours and a
|
|
95
|
+
// stale set held that long would re-dispatch entities it saw as missing.
|
|
96
|
+
let cachedMissingOutputs = null
|
|
97
|
+
|
|
98
|
+
// The entity ids whose last render wrote a file that is no longer on disk.
|
|
99
|
+
//
|
|
100
|
+
// One stat per recorded snapshot. The syscalls are ~2.2ms per 1842 paths and
|
|
101
|
+
// cost the same whether the files are there or not; at the build level it does
|
|
102
|
+
// not register — warm rebuilds of the 10k perf corpus ran 3.36s median with
|
|
103
|
+
// this against 3.56s without, i.e. nominally faster, which is only to say the
|
|
104
|
+
// difference is inside the run-to-run spread. Re-measure with
|
|
105
|
+
// `npm run test:perf` before trusting a claim that it got slower.
|
|
106
|
+
export function missingOutputIds() {
|
|
107
|
+
if (cachedMissingOutputs) return cachedMissingOutputs
|
|
108
|
+
const missing = new Set()
|
|
109
|
+
for (const snapshot of runtime.manifest?.all?.() ?? []) {
|
|
110
|
+
if (!snapshot.destination) continue
|
|
111
|
+
if (outputMissing(snapshot.destination)) missing.add(snapshot.id)
|
|
112
|
+
}
|
|
113
|
+
cachedMissingOutputs = missing
|
|
114
|
+
return missing
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function forgetMissingOutputs() {
|
|
118
|
+
cachedMissingOutputs = null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// What overrides a gate's own evidence, or null when nothing does.
|
|
122
|
+
//
|
|
123
|
+
// Callers pass the evidence they hold: `reload` if they are a watch event,
|
|
124
|
+
// `id` if they are gating an entity whose outputs are recorded. Passing
|
|
125
|
+
// neither asks only the universal question, which is what the render gate
|
|
126
|
+
// wants — it checks the output separately and later, so that an entity whose
|
|
127
|
+
// inputs ALSO moved reports `inputs-changed` rather than losing that detail
|
|
128
|
+
// to a broader answer.
|
|
129
|
+
export function bypassReason({ reload = false, id } = {}) {
|
|
130
|
+
if (reload) return REASON.RELOAD
|
|
131
|
+
if (runtime.options?.force) return REASON.FORCE
|
|
132
|
+
if (runtime.catalog?.cacheInvalidated) return REASON.CACHE_INVALIDATED
|
|
133
|
+
if (id !== undefined && missingOutputIds().has(id)) return REASON.OUTPUT_MISSING
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Is every entity being presented for evaluation this cycle?
|
|
138
|
+
//
|
|
139
|
+
// The same fact as bypassReason, asked for a different purpose: a "declared X
|
|
140
|
+
// matched nothing" warning is only worth printing when everything was looked
|
|
141
|
+
// at. On an incremental cycle a pattern legitimately matches nothing in a run
|
|
142
|
+
// of two, and a warning that always fires is one people filter out — taking
|
|
143
|
+
// the real instance with it.
|
|
144
|
+
//
|
|
145
|
+
// `firstRun` counts here and not in bypassReason because there is nothing to
|
|
146
|
+
// bypass on a first run: no prior checksum, no snapshot, so every gate opens
|
|
147
|
+
// on its own evidence.
|
|
148
|
+
//
|
|
149
|
+
// Takes the runtime rather than reading the singleton, so a plugin passes the
|
|
150
|
+
// one it was injected with — the singleton in a real build, the harness's
|
|
151
|
+
// stand-in under test.
|
|
152
|
+
export function isFullCycle(engineRuntime = runtime) {
|
|
153
|
+
return !!(
|
|
154
|
+
engineRuntime?.options?.force
|
|
155
|
+
|| engineRuntime?.options?.firstRun
|
|
156
|
+
|| engineRuntime?.catalog?.cacheInvalidated
|
|
157
|
+
)
|
|
158
|
+
}
|
package/src/manifest.js
CHANGED
|
@@ -60,6 +60,9 @@ import { extractRefs, inputHashOf, inputPartsOf, diffInputParts, lookupKeys } fr
|
|
|
60
60
|
import { filterKey } from './track.js'
|
|
61
61
|
import { findById, findEntities } from './catalog.js'
|
|
62
62
|
import { useDatabase, registerSchema } from './database/index.js'
|
|
63
|
+
import {
|
|
64
|
+
REASON, bypassReason, resolveOutputPath, outputMissing, missingOutputIds, forgetMissingOutputs,
|
|
65
|
+
} from './invalidation.js'
|
|
63
66
|
|
|
64
67
|
// Schema registration. Applied at db.open(). PRIMARY KEY (id,
|
|
65
68
|
// destination) — leading id column means `WHERE id = ?` queries use the
|
|
@@ -284,13 +287,6 @@ function snapToRow(snap) {
|
|
|
284
287
|
// becomes two symptoms — one loud (every asset reported missing, at its
|
|
285
288
|
// real and present path) and one silent (no outputHash recorded, leaving
|
|
286
289
|
// most snapshots presence-checked only).
|
|
287
|
-
export function resolveOutputPath(destination, outputFolder = runtime.options?.outputFolder) {
|
|
288
|
-
if (!destination) return undefined
|
|
289
|
-
const joined = path.join(outputFolder ?? '', destination)
|
|
290
|
-
if (existsSync(joined)) return joined
|
|
291
|
-
if (path.isAbsolute(destination) && existsSync(destination)) return destination
|
|
292
|
-
return joined
|
|
293
|
-
}
|
|
294
290
|
|
|
295
291
|
async function hashOutputFile(destination) {
|
|
296
292
|
const filePath = resolveOutputPath(destination)
|
|
@@ -372,11 +368,6 @@ export async function sourcesOf(destination) {
|
|
|
372
368
|
export function createManifest(db) {
|
|
373
369
|
if (!db) throw new Error('createManifest: db is required')
|
|
374
370
|
|
|
375
|
-
// Per-instance, not module-level: tests build their own manifest via
|
|
376
|
-
// createManifest(db), and a shared memo would leak one test's output
|
|
377
|
-
// folder into the next.
|
|
378
|
-
let cachedMissingOutputs = null
|
|
379
|
-
|
|
380
371
|
const stmtCollisions = db.prepare(`
|
|
381
372
|
SELECT destination, count(*) AS n, group_concat(id) AS ids
|
|
382
373
|
FROM mikser_snapshots
|
|
@@ -682,8 +673,12 @@ export function createManifest(db) {
|
|
|
682
673
|
// That is the situation --force exists for — the invalidation
|
|
683
674
|
// graph being under suspicion — including where the preset
|
|
684
675
|
// no-match warning tells the operator to use it.
|
|
685
|
-
|
|
686
|
-
|
|
676
|
+
// --force and a wiped cache both mean "ignore what you think you
|
|
677
|
+
// know", and both are declared in invalidation.js so a third one
|
|
678
|
+
// added there reaches this gate without being remembered.
|
|
679
|
+
const override = bypassReason()
|
|
680
|
+
if (override) return { skip: false, reason: override }
|
|
681
|
+
if (entity?.meta?.cache === false) return { skip: false, reason: REASON.CACHE_DISABLED }
|
|
687
682
|
// A render whose last attempt threw must be retried, and checked
|
|
688
683
|
// before anything else: every other branch reasons about hashes,
|
|
689
684
|
// and the hashes are consistent — the entity did not change, the
|
|
@@ -737,15 +732,13 @@ export function createManifest(db) {
|
|
|
737
732
|
// rather than in the render loop because this is the function that
|
|
738
733
|
// owns the word: anything asking shouldSkip() gets the same answer.
|
|
739
734
|
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
// the same
|
|
743
|
-
//
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
if (snapshot.destination && !existsSync(resolveOutputPath(snapshot.destination))) {
|
|
748
|
-
return { skip: false, reason: 'output-missing', destination: snapshot.destination }
|
|
735
|
+
// Asked of invalidation.js rather than answered here, so that
|
|
736
|
+
// auditOutput, the source gate and the assets marker resolve a
|
|
737
|
+
// destination the same way. The two disagreeing is the bug —
|
|
738
|
+
// `--audit-output` finding what the build just called clean is
|
|
739
|
+
// what sends people to this function in the first place.
|
|
740
|
+
if (outputMissing(snapshot.destination)) {
|
|
741
|
+
return { skip: false, reason: REASON.OUTPUT_MISSING, destination: snapshot.destination }
|
|
749
742
|
}
|
|
750
743
|
if (!snapshot.refClosure?.length) return { skip: true, reason: 'unchanged' }
|
|
751
744
|
const sourceLang = entity?.meta?.lang ?? null
|
|
@@ -1031,7 +1024,7 @@ export function createManifest(db) {
|
|
|
1031
1024
|
// rediscover the same caveat. missingOutputIds() is memoized for the
|
|
1032
1025
|
// cycle, so this shares the walk the source gate already paid for.
|
|
1033
1026
|
recordedHashes() {
|
|
1034
|
-
const missingOutputs =
|
|
1027
|
+
const missingOutputs = missingOutputIds()
|
|
1035
1028
|
const map = new Map()
|
|
1036
1029
|
for (const row of stmtEntityInputHashes.iterate()) {
|
|
1037
1030
|
if (missingOutputs.has(row.id)) continue
|
|
@@ -1106,56 +1099,6 @@ export function createManifest(db) {
|
|
|
1106
1099
|
return edges
|
|
1107
1100
|
},
|
|
1108
1101
|
|
|
1109
|
-
// The entity ids whose last render wrote a file that is no longer
|
|
1110
|
-
// on disk. A dispatch hint, the same shape as queryAffected: the
|
|
1111
|
-
// source checksum gate consults it so those entities are re-emitted
|
|
1112
|
-
// instead of being short-circuited as unchanged.
|
|
1113
|
-
//
|
|
1114
|
-
// This exists because the gate that stops `rm -rf out` from being
|
|
1115
|
-
// noticed is the FIRST one, not the last. An unchanged file never
|
|
1116
|
-
// gets a CREATE, so it never reaches the journal, so the render loop
|
|
1117
|
-
// never sees it and manifest.skipDecision — which does check for a
|
|
1118
|
-
// missing output — is never asked. Fixing only the render gate makes
|
|
1119
|
-
// the build correct for entities that get that far and changes
|
|
1120
|
-
// nothing for the case in the report, where none of them do.
|
|
1121
|
-
//
|
|
1122
|
-
// One stat per recorded snapshot, once per cycle, hoisted out of the
|
|
1123
|
-
// per-file path exactly like checksumsByCollection. The syscalls are
|
|
1124
|
-
// ~2.2ms per 1842 paths and cost the same whether the files are there
|
|
1125
|
-
// or not. At the build level it does not register: warm rebuilds of
|
|
1126
|
-
// the 10k perf corpus ran 3.36s median with this against 3.56s
|
|
1127
|
-
// without, i.e. nominally faster, which is only to say the difference
|
|
1128
|
-
// is well inside the run-to-run spread (3.0-4.1 vs 3.5-4.7 over five
|
|
1129
|
-
// runs each). Re-measure with `npm run test:perf` before trusting a
|
|
1130
|
-
// claim that it got slower.
|
|
1131
|
-
//
|
|
1132
|
-
// Resolves destinations through resolveOutputPath, which is what
|
|
1133
|
-
// auditOutput uses. The two must agree — `--audit-output` reporting
|
|
1134
|
-
// files that the build just called unchanged is the symptom, not a
|
|
1135
|
-
// separate diagnostic.
|
|
1136
|
-
// Memoized for the cycle. Two callers ask — the source checksum gate
|
|
1137
|
-
// once per scan, recordedHashes() once per layouts dispatch — and both
|
|
1138
|
-
// ask before anything has rendered, so one walk answers both. Cleared
|
|
1139
|
-
// at the end of onFinalize, which is where this cycle's renders are
|
|
1140
|
-
// recorded and the answer stops being true.
|
|
1141
|
-
missingOutputIds() {
|
|
1142
|
-
if (cachedMissingOutputs) return cachedMissingOutputs
|
|
1143
|
-
const missing = new Set()
|
|
1144
|
-
for (const row of stmtSelectAll.iterate()) {
|
|
1145
|
-
const snap = rowToSnap(row)
|
|
1146
|
-
if (!snap.destination) continue
|
|
1147
|
-
if (!existsSync(resolveOutputPath(snap.destination))) missing.add(snap.id)
|
|
1148
|
-
}
|
|
1149
|
-
cachedMissingOutputs = missing
|
|
1150
|
-
return missing
|
|
1151
|
-
},
|
|
1152
|
-
|
|
1153
|
-
// Drops the memo. Called at the end of onFinalize; exported on the
|
|
1154
|
-
// surface so a test can force a re-read without a full cycle.
|
|
1155
|
-
forgetMissingOutputs() {
|
|
1156
|
-
cachedMissingOutputs = null
|
|
1157
|
-
},
|
|
1158
|
-
|
|
1159
1102
|
// Walk the output folder against recorded snapshots, returning
|
|
1160
1103
|
// a diff describing missing / mismatched / orphaned /
|
|
1161
1104
|
// unverifiable. Backs `mikser --audit-output`. Pure: no mutations.
|
|
@@ -1543,5 +1486,5 @@ onFinalize(async () => {
|
|
|
1543
1486
|
// Dropped here rather than at the start of the next cycle because in
|
|
1544
1487
|
// watch mode there may not be one for hours, and a stale set held that
|
|
1545
1488
|
// long would keep re-dispatching entities it saw as missing.
|
|
1546
|
-
|
|
1489
|
+
forgetMissingOutputs()
|
|
1547
1490
|
})
|
package/src/plugins/assets.js
CHANGED
|
@@ -6,9 +6,10 @@ import { mkdir, writeFile, unlink, rm, readFile, symlink, } from 'fs/promises'
|
|
|
6
6
|
import { existsSync } from 'node:fs'
|
|
7
7
|
import { createRequire } from 'node:module'
|
|
8
8
|
import { globby } from 'globby'
|
|
9
|
+
import { isFullCycle, outputMissing } from '../invalidation.js'
|
|
9
10
|
import _ from 'lodash'
|
|
10
11
|
import map from 'p-map'
|
|
11
|
-
|
|
12
|
+
|
|
12
13
|
|
|
13
14
|
// Normalize a `options.presets[name]` value to a consistent
|
|
14
15
|
// { matches, options } shape so callers don't have to inspect which form
|
|
@@ -357,8 +358,10 @@ export function assets(options = {}) {
|
|
|
357
358
|
// the only thing that ever said a file was missing.
|
|
358
359
|
//
|
|
359
360
|
// Checked first and cheaply: one stat, and if the derivative is gone
|
|
360
|
-
// no amount of marker archaeology changes the answer.
|
|
361
|
-
|
|
361
|
+
// no amount of marker archaeology changes the answer. Asked of
|
|
362
|
+
// invalidation.js so a derivative and a rendered page agree on what
|
|
363
|
+
// "the output is gone" means.
|
|
364
|
+
if (outputMissing(entity.destination)) return false
|
|
362
365
|
let result = false
|
|
363
366
|
let revisions = []
|
|
364
367
|
const assetChecksum = `${entity.destination}.${entity.preset.checksum}.md5`
|
package/src/plugins/files.js
CHANGED
|
@@ -207,12 +207,9 @@ export function files(options = {}) {
|
|
|
207
207
|
// so the next one cannot be added to that place and missed here.
|
|
208
208
|
//
|
|
209
209
|
// priorChecksums is still bulk-prefetched per scan so the gate
|
|
210
|
-
// reads a map instead of doing per-file SQL
|
|
211
|
-
//
|
|
212
|
-
// latter for the cycle, so this shares the walk with useSource's
|
|
213
|
-
// own scans rather than paying for a second one.
|
|
210
|
+
// reads a map instead of doing per-file SQL. What overrides the
|
|
211
|
+
// checksum, the gate asks invalidation.js for itself.
|
|
214
212
|
const priorChecksums = checksumsByCollection(collection)
|
|
215
|
-
const missingOutputs = runtime.manifest?.missingOutputIds() ?? new Set()
|
|
216
213
|
const scanned = new Set()
|
|
217
214
|
await pMap(paths, async relativePath => {
|
|
218
215
|
const { source } = await ensureLink(relativePath)
|
|
@@ -230,7 +227,7 @@ export function files(options = {}) {
|
|
|
230
227
|
// the catalog already has this file unchanged and there is
|
|
231
228
|
// nothing to emit. Progress ticks either way — a gated file
|
|
232
229
|
// was still looked at.
|
|
233
|
-
const newChecksum = await gateChecksum(source, id, { priorChecksums
|
|
230
|
+
const newChecksum = await gateChecksum(source, id, { priorChecksums })
|
|
234
231
|
updateProgress()
|
|
235
232
|
if (newChecksum === null) return
|
|
236
233
|
await createEntity({
|
package/src/source.js
CHANGED
|
@@ -46,6 +46,7 @@ import { ACTION } from './constants.js'
|
|
|
46
46
|
import { checksum as fileChecksum, checksumOf, junkIgnore } from './utils.js'
|
|
47
47
|
import { reportGated, reportChanged } from './report.js'
|
|
48
48
|
import { findById, findEntities, checksumsByCollection } from './catalog.js'
|
|
49
|
+
import { bypassReason } from './invalidation.js'
|
|
49
50
|
import { useDatabase } from './database/index.js'
|
|
50
51
|
|
|
51
52
|
// Per-source-scan registerFile concurrency. The work is dominated by
|
|
@@ -81,20 +82,12 @@ const SCAN_CONCURRENCY = 16
|
|
|
81
82
|
// disagree, and the losing combination (empty content + a checksum correct
|
|
82
83
|
// for the finished file) is permanent, because every later sync then
|
|
83
84
|
// short-circuits on "unchanged".
|
|
84
|
-
|
|
85
|
-
// wrote a file that is no longer on disk (manifest.missingOutputIds()). The
|
|
86
|
-
// gate answers "has this INPUT changed", and deleting the output folder does
|
|
87
|
-
// not change any input — so without this, `rm -rf out` followed by a build
|
|
88
|
-
// short-circuits every file here, emits nothing, renders nothing, and reports
|
|
89
|
-
// success over an empty folder. An entity whose output is gone is not
|
|
90
|
-
// unchanged in any sense the caller means.
|
|
91
|
-
export async function gateChecksum(file, id, { reload = false, priorChecksums, bytes, missingOutputs } = {}) {
|
|
85
|
+
export async function gateChecksum(file, id, { reload = false, priorChecksums, bytes } = {}) {
|
|
92
86
|
const compute = () => (bytes !== undefined ? checksumOf(bytes) : fileChecksum(file))
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (canGate) {
|
|
87
|
+
// What overrides the checksum is not this function's to decide — see
|
|
88
|
+
// invalidation.js. It used to be, and the copy in files.js then missed
|
|
89
|
+
// the output-existence clause when that was added here.
|
|
90
|
+
if (!bypassReason({ reload, id })) {
|
|
98
91
|
const priorChecksum = priorChecksums
|
|
99
92
|
? priorChecksums.get(id)
|
|
100
93
|
: findById(id)?.checksum
|
|
@@ -430,12 +423,6 @@ export function useSource(core, options) {
|
|
|
430
423
|
// per-file instead of doing per-file SQL lookups. ~14× faster
|
|
431
424
|
// at 14k entities (column projection vs full-entity JSON.parse).
|
|
432
425
|
const priorChecksums = checksumsByCollection(collection)
|
|
433
|
-
// Prefetched once per scan, beside priorChecksums and for the same
|
|
434
|
-
// reason: the gate needs an O(1) answer per file, and this is one
|
|
435
|
-
// pass over the snapshots rather than a stat inside the per-file
|
|
436
|
-
// path. Entities listed here defeat the gate — their input is
|
|
437
|
-
// unchanged but the file they produced is gone.
|
|
438
|
-
const missingOutputs = runtime.manifest?.missingOutputIds() ?? new Set()
|
|
439
426
|
// Parallel register — file read + checksum is I/O-bound. Set
|
|
440
427
|
// additions, scanStats increments, and journal addEntry calls
|
|
441
428
|
// are all single-threaded-JS-atomic so no locking required;
|
|
@@ -443,7 +430,7 @@ export function useSource(core, options) {
|
|
|
443
430
|
// and INSERTs are ~10μs vs ~ms-per-file-read, so the journal
|
|
444
431
|
// is never the bottleneck.
|
|
445
432
|
await pMap(files, async (file) => {
|
|
446
|
-
await registerFile(file, { logger, scanned, stats: scanStats, priorChecksums
|
|
433
|
+
await registerFile(file, { logger, scanned, stats: scanStats, priorChecksums })
|
|
447
434
|
if (phase === 'import') updateProgress()
|
|
448
435
|
}, { concurrency: SCAN_CONCURRENCY })
|
|
449
436
|
|
|
@@ -464,7 +451,7 @@ export function useSource(core, options) {
|
|
|
464
451
|
logger.info(scanSummary({ cap, loaded: files.length, ...scanStats }))
|
|
465
452
|
})
|
|
466
453
|
|
|
467
|
-
async function registerFile(file, { logger, action = ACTION.CREATE, scanned, priorChecksums
|
|
454
|
+
async function registerFile(file, { logger, action = ACTION.CREATE, scanned, priorChecksums } = {}) {
|
|
468
455
|
// stats — when called from the scanHook the outer scan provides
|
|
469
456
|
// a Map to accumulate counts. For onSync (chokidar single-file
|
|
470
457
|
// events) the counter is absent and per-file tally is not
|
|
@@ -497,7 +484,7 @@ export function useSource(core, options) {
|
|
|
497
484
|
}
|
|
498
485
|
}
|
|
499
486
|
|
|
500
|
-
const chksum = await gateChecksum(file, id, { reload, priorChecksums, bytes
|
|
487
|
+
const chksum = await gateChecksum(file, id, { reload, priorChecksums, bytes })
|
|
501
488
|
if (chksum === null) {
|
|
502
489
|
if (stats) stats.skipped++
|
|
503
490
|
// Never becomes a render task, so it would otherwise be invisible
|
package/src/utils.js
CHANGED
|
@@ -1187,29 +1187,6 @@ export async function writeOutput(file, bytes) {
|
|
|
1187
1187
|
return true
|
|
1188
1188
|
}
|
|
1189
1189
|
|
|
1190
|
-
// Did this cycle evaluate the whole corpus, or only what changed?
|
|
1191
|
-
//
|
|
1192
|
-
// The distinction is what makes a "declared X matched nothing" warning
|
|
1193
|
-
// worth printing. On an incremental cycle only changed entities are
|
|
1194
|
-
// re-evaluated, so a pattern or preset legitimately matches nothing in a
|
|
1195
|
-
// run of two — printing it there means the warning fires on every healthy
|
|
1196
|
-
// build, and a warning that always fires is one people filter out, taking
|
|
1197
|
-
// the real instance with it.
|
|
1198
|
-
//
|
|
1199
|
-
// True for: --force, a first-run or wiped database, and a cache
|
|
1200
|
-
// invalidation. Those are exactly the cases where the import gate is
|
|
1201
|
-
// bypassed and every entity is presented for evaluation.
|
|
1202
|
-
// Takes the runtime rather than reading the singleton, so a plugin passes
|
|
1203
|
-
// the one it was injected with — which is the singleton in a real build and
|
|
1204
|
-
// the harness's stand-in under test.
|
|
1205
|
-
export function isFullCycle(rt = runtime) {
|
|
1206
|
-
return !!(
|
|
1207
|
-
rt?.options?.force
|
|
1208
|
-
|| rt?.options?.firstRun
|
|
1209
|
-
|| rt?.catalog?.cacheInvalidated
|
|
1210
|
-
)
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
1190
|
// ── operating-system and file-manager litter ────────────────────────────
|
|
1214
1191
|
//
|
|
1215
1192
|
// Exposing a source folder over a network filesystem (mikser-io-drive) or
|