mikser-io 11.10.3 → 11.10.4

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": "11.10.3",
3
+ "version": "11.10.4",
4
4
  "files": [
5
5
  "app.js",
6
6
  "index.js",
@@ -53,8 +53,47 @@ onFinalize(async () => {
53
53
  const newDestinationsByParent = new Map()
54
54
  const lostPagination = new Set()
55
55
 
56
- for await (const { entity } of useJournal('Manifest cleanup', [OPERATION.DELETE])) {
57
- deletedIds.push(entity.id)
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