All playbooksNative Utility

Add a job handler

Run long product work as a job in its own crate, with checkpoints for pause and cancel, progress events, stable error codes and tests through the public app-api.

When to use it

The product does work that can take seconds or hours: reading a folder tree, importing files, talking to a device. The steps use an acme.import job in a crates/acme-import crate as the example; examples/rescuewerk shows the same pattern with a folder scan.

Rules to keep

  • Product code lives in product crates that depend on app-api; the template crates (crates/app-*) never depend on a product.
  • A command never waits for a job. start_job returns the id at once and the UI follows the events; wait_for_job is only for the CLI and tests.
  • The handler calls ctx.checkpoint()? at least once per unit of work: pause and cancel take effect only there.
  • Failures and warnings are stable RW_* codes, translated in the three locales. Never put sentences or user paths in a code.
  • A path chosen in the UI goes through validatePath before it becomes a job parameter.

Steps

  1. Create crates/acme-import/ with a Cargo.toml like examples/rescuewerk/crates/rescuewerk-scan/Cargo.toml (workspace fields, app-api and serde_json, app-config for tests). The workspace already includes crates/*. Keep product crates under crates/: the catalog test in packages/i18n/src/catalog.test.ts checks the RW_* codes found in crates/ and apps/, not in examples/.
  2. Implement JobHandler as in rescuewerk-scan/src/lib.rs and docs/job-engine.md: read and check ctx.params(), failing with JobError::failed("RW_IMPORT_...") on invalid input; call ctx.checkpoint()? in the loop; report ctx.progress(done, total) (None while the total is unknown, the final value at the end); use ctx.warn for items that can be skipped. Export the kind as a constant, for example pub const IMPORT_JOB_KIND: &str = "acme.import";.
  3. Test the job in crates/acme-import/tests/ through AppApi, as rescuewerk-scan/tests/scan.rs does: build the API in_memory() with .job_handler(IMPORT_JOB_KIND, Arc::new(ImportJob)), start it, wait with wait_for_job and check the final state, done/total and the error_code for invalid input. Use files under fixtures/, never paths of the developer's machine.
  4. Register the handler in the shell: add acme-import = { path = "crates/acme-import" } to [workspace.dependencies] in Cargo.toml and acme-import.workspace = true to apps/desktop/src-tauri/Cargo.toml, then call .job_handler(...) on the builder in apps/desktop/src-tauri/src/main.rs. Do the same in apps/cli if the CLI must run it, and commit the updated Cargo.lock (CI runs cargo fetch --locked). No new Tauri command is needed: start_job, pause_job, resume_job and cancel_job already exist.
  5. In the UI build the request in a service of the feature, like sampleJobRequest in apps/desktop/src/features/jobs/services/job-requests.ts: validate and clamp the parameters, and resolve paths with validatePath first. Start it with startJob and follow it through the jobs queries that the events already update.
  6. Teach MockNativeClient in packages/native-client/src/mock.ts the new kind (it rejects unknown kinds with RW_JOB_KIND_UNKNOWN), so pnpm dev and the component tests work without Rust.
  7. Add every new RW_* code to packages/i18n/locales/*/errors.json in de-DE, en and it-IT, and the UI texts to common.json.

Done when

  • the verification commands pass;
  • the job can be paused, resumed and cancelled while it runs, and reaches COMPLETED or FAILED with a translated code;
  • the integration test runs through AppApi without network or machine paths;
  • every new code exists in the three locales.