mikser-io 10.0.2 → 10.0.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mikser-io",
3
- "version": "10.0.2",
3
+ "version": "10.0.3",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
package/src/manifest.js CHANGED
@@ -1122,9 +1122,12 @@ export function createManifest(db) {
1122
1122
  // One stat per recorded snapshot, once per cycle, hoisted out of the
1123
1123
  // per-file path exactly like checksumsByCollection. The syscalls are
1124
1124
  // ~2.2ms per 1842 paths and cost the same whether the files are there
1125
- // or not; at the build level a warm 2000-document rebuild measured
1126
- // 1.05s against 1.08s without it, which is inside the run-to-run
1127
- // spread the walk disappears into the cycle it is part of.
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.
1128
1131
  //
1129
1132
  // Resolves destinations through resolveOutputPath, which is what
1130
1133
  // auditOutput uses. The two must agree — `--audit-output` reporting
@@ -348,6 +348,17 @@ export function assets(options = {}) {
348
348
  }
349
349
 
350
350
  async function isPresetRendered(entity) {
351
+ // The marker says a render HAPPENED. It does not say the file it
352
+ // produced is still there, and the two come apart the moment anyone
353
+ // deletes a derivative — which is the obvious thing to do when you
354
+ // want one rebuilt. The marker survives, this returned true, and the
355
+ // image never came back on any number of ordinary builds; only
356
+ // --force or a config change brought it round, and --audit-output was
357
+ // the only thing that ever said a file was missing.
358
+ //
359
+ // Checked first and cheaply: one stat, and if the derivative is gone
360
+ // no amount of marker archaeology changes the answer.
361
+ if (!existsSync(entity.destination)) return false
351
362
  let result = false
352
363
  let revisions = []
353
364
  const assetChecksum = `${entity.destination}.${entity.preset.checksum}.md5`
@@ -4,7 +4,7 @@ import { mkdir, symlink, unlink, lstat, realpath } from 'fs/promises'
4
4
  import { globby } from 'globby'
5
5
  import pMap from 'p-map'
6
6
  import { checksumsByCollection } from '../catalog.js'
7
- import { sweepDeleted } from '../source.js'
7
+ import { gateChecksum, sweepDeleted } from '../source.js'
8
8
 
9
9
  export function files(options = {}) {
10
10
  return ({
@@ -187,23 +187,32 @@ export function files(options = {}) {
187
187
 
188
188
  const paths = await globby('**/*', { cwd: runtime.options.filesFolder })
189
189
  trackProgress('Files import', paths.length)
190
- // Bulk-prefetch the catalog's existing (id checksum) map for
191
- // this collection once at scan start, so the gate below reads
192
- // from it per-file instead of doing per-file SQL lookups. Same
193
- // pattern source.js uses for documents/layouts via useSource.
194
- // Without this gate the plugin re-emitted createEntity on every
195
- // cycle for every file regardless of changes, inflating the
196
- // journal with phantom mutations and triggering downstream
197
- // re-dispatch of aggregate layouts whose recorded query deps
198
- // matched the collection.
199
- // --force (and a wiped catalog) must defeat this gate, the
200
- // same way source.js's gateChecksum lets them defeat its own.
201
- // This is a second, independent gate: if it ignores them, no
202
- // amount of forcing re-derives a file's name / meta.url /
203
- // meta.presets, and a catalog holding bad `files` rows has no
204
- // repair path short of deleting them.
205
- const forced = runtime.options.force || runtime.catalog?.cacheInvalidated
190
+ // The SHARED gate, not a copy of it.
191
+ //
192
+ // Without a gate the plugin re-emitted createEntity on every cycle
193
+ // for every file regardless of changes, inflating the journal with
194
+ // phantom mutations and triggering downstream re-dispatch of
195
+ // aggregate layouts whose recorded query deps matched the
196
+ // collection. So it grew one — and then it was a SECOND,
197
+ // independent implementation of the decision source.js already
198
+ // owns, which is how it silently missed the missing-output bypass
199
+ // that was added to the original: an image derivative deleted from
200
+ // the assets folder was never regenerated, because the file it
201
+ // derives from was gated here as unchanged and never reached the
202
+ // assets plugin. Green build, missing image, and only
203
+ // --audit-output said otherwise.
204
+ //
205
+ // Every reason to defeat the gate — --force, a wiped catalog, a
206
+ // recorded output that is gone — now lives in exactly one place,
207
+ // so the next one cannot be added to that place and missed here.
208
+ //
209
+ // priorChecksums is still bulk-prefetched per scan so the gate
210
+ // reads a map instead of doing per-file SQL, and missingOutputs
211
+ // alongside it for the same reason; the manifest memoizes the
212
+ // latter for the cycle, so this shares the walk with useSource's
213
+ // own scans rather than paying for a second one.
206
214
  const priorChecksums = checksumsByCollection(collection)
215
+ const missingOutputs = runtime.manifest?.missingOutputIds() ?? new Set()
207
216
  const scanned = new Set()
208
217
  await pMap(paths, async relativePath => {
209
218
  const { source } = await ensureLink(relativePath)
@@ -217,15 +226,13 @@ export function files(options = {}) {
217
226
  // to re-emit; the sweep asks what still EXISTS, and a file
218
227
  // skipped for being unchanged very much exists.
219
228
  scanned.add(id)
220
- const newChecksum = await checksum(source)
229
+ // Returns the checksum to stamp on the entity, or null when
230
+ // the catalog already has this file unchanged and there is
231
+ // nothing to emit. Progress ticks either way — a gated file
232
+ // was still looked at.
233
+ const newChecksum = await gateChecksum(source, id, { priorChecksums, missingOutputs })
221
234
  updateProgress()
222
- // Gate: if the catalog already has this entity with the same
223
- // checksum, the file hasn't changed since the last cycle.
224
- // Skip emitting a CREATE — the catalog row stays correct,
225
- // the journal stays accurate (mutations = actual changes),
226
- // and downstream aggregate-layout invalidation isn't fired
227
- // spuriously.
228
- if (!forced && priorChecksums.get(id) === newChecksum) return
235
+ if (newChecksum === null) return
229
236
  await createEntity({
230
237
  id,
231
238
  uri,