Skip to content

Refactor job output plumbing - #4500

Open
infomiho wants to merge 11 commits into
mainfrom
miho/job-output-plumbing
Open

Refactor job output plumbing#4500
infomiho wants to merge 11 commits into
mainfrom
miho/job-output-plumbing

Conversation

@infomiho

@infomiho infomiho commented Jul 17, 2026

Copy link
Copy Markdown
Member

Description

Split from #4407 so the Job refactor can be reviewed separately from the server lifecycle change.

The new Job model

A Job is one complete top-level operation, such as building the SDK or running the server. It owns:

  • a JobKind, used to label its output;
  • a JobAction, which can emit output, fail with an exit code, and register cleanup work;
  • a stream of JobEvents: output while the Job is running, followed by one exit event when it finishes.

Only the Job runner emits the exit event. A subprocess can forward stdout and stderr, but its exit does not pretend that the whole Job ended.

Each Job runs in its own ResourceT, so registered resources are released before the Job reports that it exited. Cancellation releases resources without reporting a normal exit.

Subprocesses and output

Wasp.Job.Subprocess.run handles a finite child process and returns its exit code inside JobAction. Wasp.Job.Node builds on that for Node and npm commands.

Output reading carries incomplete UTF-8 bytes between chunks, so a character split across two reads is decoded correctly.

Review map

Mostly code moving or renaming:

  • Wasp.Job.IO.PrefixedWriter moves to Wasp.Job.Output.Prefixed. Most edits there only adapt JobMessage/JobType names to JobEvent/JobKind.
  • Node command helpers move out of the general Wasp.Job module and into Wasp.Job.Node.
  • Most Generator and CLI edits are mechanical call-site migrations to runJob, JobAction, and the new output helpers.

New behavior worth reviewing:

  • A failed required subprocess stops the rest of its JobAction.
  • Job resources are released before the exit event is emitted.
  • Cancelling a Job releases its resources without emitting a misleading exit event.
  • Finite subprocess output handles split UTF-8 correctly.

Type of change

  • 🔧 Just code/docs improvement
  • 🐞 Bug fix
  • 🚀 New/improved feature
  • 💥 Breaking change

Checklist

  • I tested my change in a Wasp app to verify that it works as intended.

  • 🧪 Tests and apps:

    • I added unit tests for my change.
    • (if you fixed a bug) I added a regression test for the bug I fixed.
    • (if you added/updated a feature) I added/updated e2e tests in examples/kitchen-sink/e2e-tests.
    • (if you added/updated a feature) I updated the starter templates in waspc/data/Cli/templates, as needed.
    • (if you added/updated a feature) I updated the example apps in examples/, as needed.
      • (if you updated examples/tutorials) I updated the tutorial in the docs (and vice versa).
  • 📜 Documentation:

    • (if you added/updated a feature) I added/updated the documentation in web/docs/.
  • 🆕 Changelog: (if change is more than just code/docs improvement)

    • I updated waspc/ChangeLog.md with a user-friendly description of the change.
    • (if you did a breaking change) I added a step to the current migration guide in web/docs/migration-guides/.
    • I bumped the version in waspc/waspc.cabal to reflect the changes I introduced.

@infomiho
infomiho requested a review from a team as a code owner July 17, 2026 13:03
@infomiho
infomiho requested review from FranjoMindek and removed request for a team July 17, 2026 13:03
@infomiho
infomiho temporarily deployed to fly-deploy-test July 17, 2026 13:09 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 17, 2026 13:09 — with GitHub Actions Inactive
@pkg-pr-new

pkg-pr-new Bot commented Jul 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

@wasp.sh/spec

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/spec@4500

@wasp.sh/wasp-cli

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli@4500

@wasp.sh/wasp-cli-darwin-arm64-unknown

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-darwin-arm64-unknown@4500

@wasp.sh/wasp-cli-darwin-x64-unknown

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-darwin-x64-unknown@4500

@wasp.sh/wasp-cli-linux-arm64-glibc

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-linux-arm64-glibc@4500

@wasp.sh/wasp-cli-linux-x64-glibc

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-linux-x64-glibc@4500

@wasp.sh/wasp-cli-linux-x64-musl

npx https://pkg.pr.new/wasp-lang/wasp/@wasp.sh/wasp-cli-linux-x64-musl@4500

commit: d9d2b97

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploying wasp-docs-on-main with  Cloudflare Pages  Cloudflare Pages

Latest commit: fe3052f
Status: ✅  Deploy successful!
Preview URL: https://7cdf1934.wasp-docs-on-main.pages.dev
Branch Preview URL: https://miho-job-output-plumbing.wasp-docs-on-main.pages.dev

View logs

@infomiho
infomiho temporarily deployed to fly-deploy-test July 17, 2026 13:45 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 17, 2026 13:45 — with GitHub Actions Inactive
@infomiho
infomiho force-pushed the miho/job-output-plumbing branch from e744a06 to 0f434bd Compare July 20, 2026 13:52
@infomiho
infomiho temporarily deployed to fly-deploy-test July 20, 2026 14:06 — with GitHub Actions Inactive
@infomiho
infomiho had a problem deploying to railway-deploy-test July 20, 2026 14:06 — with GitHub Actions Failure
@infomiho
infomiho temporarily deployed to fly-deploy-test July 20, 2026 15:04 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 20, 2026 15:04 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 21, 2026 09:29 — with GitHub Actions Inactive

@cprecioso cprecioso left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if we're re-inventing involved code for what is actually somewhat generic subprocess handling.

The JobOutputSink is a bit nebulous. I had a hard time grasping that in the end is just a function that will do some side-effect on output, mainly logging or collecting, and that by virtue of being a function we can "tap" into that stream and do extra side effects on specific output (e.g. triggering an error or a recompile). Fine, a bit underexplained in the current state of the code).

But then, we only build such a sink in one place in the code, the same as where the type is defined, but it feels like the type is leaking everywhere else in the code (also taking about the next PR).

[look at comment below and come back -- storytelling ✨]

So I'm thinking that we could scrap the JobOutputSink and fold it into Job, so it doesn't leak out by default. It feels like we'd like something like WriterT (actually, we want streaming output so we'd have to use conduit, which we already depend on), so we'll need to turn Job into a monad and that will buy us the programmatic behaviors we want.

In the end I think I'd like to, rather, turn Job into

type Job = ExceptT JobExit (ConduitT String (JobOutputType, String) IO ExitCode

which would allow us to reduce our NIH boilerplate code and just use common facilities for what we want to do. There's also some Subprocess stuff in conduit-extra (which we also already depend on).


If you're not convinced, at least I'd like to see the JobOutputType concept better explained, and folded into Job in some way so that this very specific (IMO) concern doesn't have to leak everywhere.

Comment thread waspc/src/Wasp/Job.hs Outdated
Comment thread waspc/src/Wasp/Job/Node.hs Outdated
@infomiho
infomiho temporarily deployed to railway-deploy-test July 27, 2026 08:40 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 27, 2026 08:40 — with GitHub Actions Inactive
@infomiho
infomiho force-pushed the miho/job-output-plumbing branch from d39a104 to 4761c1d Compare July 27, 2026 09:36
@infomiho
infomiho temporarily deployed to railway-deploy-test July 27, 2026 09:45 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 27, 2026 10:06 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 28, 2026 11:03 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 28, 2026 11:03 — with GitHub Actions Inactive
@infomiho
infomiho requested a review from cprecioso July 28, 2026 14:29
@infomiho
infomiho temporarily deployed to railway-deploy-test July 28, 2026 14:30 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 28, 2026 16:11 — with GitHub Actions Inactive
@infomiho
infomiho had a problem deploying to railway-deploy-test July 28, 2026 16:11 — with GitHub Actions Failure
@infomiho
infomiho force-pushed the miho/job-output-plumbing branch from 8c28e16 to 16d9bdb Compare July 28, 2026 17:19
@infomiho
infomiho had a problem deploying to railway-deploy-test July 28, 2026 17:53 — with GitHub Actions Failure
@infomiho
infomiho temporarily deployed to fly-deploy-test July 28, 2026 17:53 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 29, 2026 07:48 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to fly-deploy-test July 29, 2026 08:32 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 29, 2026 08:32 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 29, 2026 09:46 — with GitHub Actions Inactive
@infomiho
infomiho force-pushed the miho/job-output-plumbing branch from b1371f6 to fe3052f Compare July 29, 2026 10:02
@infomiho
infomiho temporarily deployed to railway-deploy-test July 29, 2026 10:19 — with GitHub Actions Inactive

@cprecioso cprecioso left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments/proposals/questions on naming, architecture and semantics.
Most of them are nitpicks or proposals that you might have already tried so you know it doesn't work, I trust your judgement so do what you want.

In general, I'm happy that you effectively controlled the spread of JobOutputType. In exchange, it's the API does feel a bit too extensive now. 6 data types for what amounts to a stream of [{stdout, stderr}, String] and a final ExitCode. And the Job/Subprocess differentiation feels a bit arcane. As I commented below, I think there are conceptual simplifications hiding in places.

But also, I trust you've fought against the tide here so you know best what is possible, feasible, and understandable. Feel free to call me out when needed.

Comment thread waspc/src/Wasp/Job/Internal.hs Outdated
Comment thread waspc/src/Wasp/Job/Internal.hs Outdated
Comment thread waspc/src/Wasp/Job/Internal.hs Outdated
Comment thread waspc/src/Wasp/Job/Internal.hs Outdated
Comment thread waspc/src/Wasp/Job/Internal.hs Outdated
newtype JobFailure = JobFailure Int

newtype JobOutputSink = JobOutputSink
{ writeJobOutput :: JobOutputStream -> Text -> IO ()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This API is named as if consumers will use it directly, but not very ergonomic, as it has to getJobOutputSink first always when it just wants to write JobOutput.

Instead I would actually name this one getJobOutputSink or whatever, and then make a writeJobOutput = getJobOutputSink . ask function that deals with getting the sink directly

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mmm i'm thinking you might not be able to do that because you'd be running inside the monad and blocking other stuff, but you want to do stuff concurrently. So RAW.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

emitJobOutput is now the normal API and gets the sink itself. The raw sink functions stay internal for background IO workers. I tried running those workers directly as concurrent JobActions, but that stack cannot provide the MonadUnliftIO instance the async API needs.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is a bit meh, feels like there's a better API from the Job side hiding in here. But you know best, RAW.

@infomiho infomiho Jul 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right that there was an unnecessary layer here. ExceptT was being added and immediately removed, so I changed JobExecution to return IO (Either String ()) directly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me see if I can also get rid of this module.

Comment thread waspc/src/Wasp/Job/Output.hs Outdated
Comment on lines +25 to +32
printEventsPrefixedUntilExit :: Chan Job.JobEvent -> IO ()
printEventsPrefixedUntilExit chan = runPrefixedWriter go
where
go = do
event <- liftIO $ readChan chan
case Job._eventData event of
Job.JobOutput {} -> printEventPrefixed event >> go
Job.JobExited {} -> return ()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion:

instead of

printEventsUntilExit foo
printEventsPrefixedUntilExit bar

why not

printEventsUntilExit foo
printEventsUntilExit $ prefixEvents <$> bar

(i'm not sure if the <$> is possibe here but a regular $ would probably be)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also then you can move it somewhere associated with JobType, because Output.printPrefixed immediately makes me ask "what are we prefixing it with" but Output.print $ JobType.prefix makes it clear what I'm working with.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The exact <$> version does not work because Chan is not a functor, but the shared-loop idea does. I added one consumeEventsUntilExit helper and pass either printer to it. I kept prefix rendering under Output because it also tracks stderr, interrupted lines, previous events, output handles, and pending newlines; only the label and color come from JobKind.

Comment thread waspc/src/Wasp/Job/Subprocess/Finite.hs Outdated
Comment on lines +17 to +19
-- TODO:
-- Switch from Data.Conduit.Process to Data.Conduit.Process.Typed.
-- It is a new module meant to replace Data.Conduit.Process which is about to become deprecated.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this TODO is too big to do now? Write an issue for it maybe?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an old TODO and I investigated rewriting to CreateProcess configuration obj from Typed module, but it looked like a big refactor and I wanted to control scope (as much as I can lol). I'll create an issue, good call out!

@infomiho infomiho Jul 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opened #4575 for it and linked the TODO.

@infomiho

Copy link
Copy Markdown
Member Author

The main distinction is lifecycle: a Job is something we observe once and give one final result, while a subprocess is a child resource the Job can stop, replace, or recover from without ending. The Server Job needs that split because its server process can restart many times. I simplified the smaller API pieces you pointed out, but kept this boundary.

@infomiho
infomiho temporarily deployed to fly-deploy-test July 29, 2026 14:54 — with GitHub Actions Inactive
@infomiho
infomiho temporarily deployed to railway-deploy-test July 29, 2026 14:54 — with GitHub Actions Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants