Skip to content

feat(translator): carry the job id through the translation lifecycle - #172

Closed
SearheiParkhamchuk wants to merge 6 commits into
mainfrom
feat/translator-job-id-through-lifecycle
Closed

SearheiParkhamchuk wants to merge 6 commits into
mainfrom
feat/translator-job-id-through-lifecycle

Conversation

@SearheiParkhamchuk

@SearheiParkhamchuk SearheiParkhamchuk commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

The plugin had a job id and threw it away. Four issues from one consumer building an
editorial workflow on top of it all reduce to that.

Issues: #107, #110, #106, #103.

What changes

  • enqueue answers with what it created — one entry per requested target locale,
    because a single job row covers a document's whole locale list. TaskRunner.enqueue
    widens its return rather than changing it; the old void answer still satisfies the
    contract and is deprecated for the next major. An overload pair cannot be used here: an
    implementation must satisfy every overload, so it would break every third-party runner.
  • Lifecycle callbacks carry the job and the attempt. The task handler now asks Payload
    for the job row. The attempt is counted per locale, not per job — one row carries every
    locale of a document, so the row's own counter would report a locale's first run as the
    job's third.
  • Cancellation says what is stopping, once per target locale, before the row is
    deleted. The cancel route holds only ids, so this needs a read: a new optional
    findByIds on TaskRunner. A runner that omits it keeps working and never fires the
    callback.
  • Collection status names the document and locale beside the job id, which a caller
    previously could not resolve back to anything.

Plus one fix that fell out of the work: a job that ran out of retries kept hasError and
never got a completion date, so it stayed in the plugin's live set while Payload's own
picker excluded it for good. Asking again for a locale whose job had given up was absorbed
onto that job and silently produced no work at all.

Public API

New: EnqueueResult, TaskRunner (both now exported so an implementation outside this
package can name what it has always had to satisfy), LegacyVoidEnqueue (deprecated),
TaskRunner.findByIds, TranslationLifecycleCallbacks.onCancelled, and jobId /
attempt on TranslationTask and TaskHandlerInput. Everything additive; a host that
registers nothing new sees no change.

One deliberate exception, and it is a behaviour change: a request for a locale whose job
gave up now gets a fresh job and is translated, where before it was lost.

Verification

Check Result
unit 1978 passed, 143 files
check-types (plugin + dev app) clean
tsc declaration build green
oxlint 55 / 0 and 5 / 0, all pre-existing
integration — SQLite 197 passed / 7 skipped / 204
integration — SQLite, exclusive queue 197 / 7 / 204
integration — PostgreSQL 203 / 1 / 204
integration — MongoDB 196 / 8 / 204

The tsc line is listed separately on purpose: tsgo has passed on this package while the
real declaration build failed.

Worth a reviewer's attention

  • TaskRunner is implemented outside this package. Both contract changes widen rather than
    replace, and both carry a ledger entry in docs/DEPRECATIONS.md.
  • canStillRun is deliberately narrower than Payload's picker: processing and a future
    waitUntil mean "not runnable right now", which is exactly the job a new request may
    still join.
  • onQueued carries no job id. The decorator fires it before delegating, which is what
    keeps a synchronous runner's completed from arriving first — and at that moment no job
    exists.

Left for follow-ups

Abandoned job rows are never deleted — Payload removes a job only on success. This change
stops them being loaded and stops them hosting a request, but disposal is a retention
policy (when a dead row may go, whether its failure must outlive it, who removes it) and
belongs in its own issue.

Not included

README.md. The proposed sentences are ready and are yours to place.

…again

A job that ran out of retries keeps `hasError` and never gets a completion date, so it
stayed in the plugin's live set while Payload's own picker excluded it for good. Asking
again for a locale whose job had given up was therefore absorbed onto that job and
silently produced no work at all.

`canStillRun` is now the single predicate behind host selection and the re-read after a
write, and the enqueue scan filters the same rows in the database rather than loading
them. Deliberately narrower than Payload's picker: `processing` and a future `waitUntil`
mean "not right now", which is exactly the job a new request may still join.
Closes #107.

One entry per requested target locale, since a single job row covers a document's whole
locale list: two locales of one document name the same job, and a request spanning several
documents still reads unambiguously.

`TaskRunner.enqueue` widens its return rather than changing it — the old `void` answer
still satisfies the contract, now deprecated for the next major. An overload pair cannot
be used here: an implementation must satisfy every overload, so it would break every
third-party runner.

`TaskRunner` and `EnqueueResult` are exported so an implementation outside this package
can name what it has always had to satisfy.
…le callbacks

Closes #110.

The task handler now asks Payload for the job row, so a callback can name the job that
caused it and which attempt it is on. Together with the target locale the callback already
carried, that is what lets a host join a callback to the request behind it.

The attempt is counted per locale, not per job: one row carries every locale of a
document, so the row's own counter would report a locale's first run as the job's third.
Payload writes each counter only after a run finishes, hence the `+ 1`.

`onQueued` carries no job id — the decorator fires it before delegating, which is what
keeps a synchronous runner's `completed` from arriving first, and at that moment no job
exists.
Closes #106.

Announced once per target locale the cancelled job covered, before the row is deleted —
after the delete there is nothing left to say it from. The cancel route holds only ids, so
naming the document and locale needs a read: a new optional `findByIds` on `TaskRunner`.

A runner that omits it keeps working and simply never fires the callback. A failed read is
logged and never costs the caller their cancellation — otherwise the silence would be
indistinguishable from an id that was not ours.

The decorator is now applied for any callback it fires, not just `onQueued`; a host that
registered only `onCancelled` used to be handed an undecorated runner and heard nothing.
Closes #103.

The response carried a job id and a status, and a caller could not resolve either back to
what was being translated. Each entry now says which document and which target locale it
is for.

The job id stays, and it repeats: one job covers a document's whole locale list, so `id`
cannot identify an entry on its own.
@vercel

vercel Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ideal-cms Ready Ready Preview Oct 6, 2026 2:38pm UTC

Request Review

…or logging why it could not

`announceCancellation` mixed three jobs — read the rows, decide whether to announce, log a
failed read — and the third forced a log-only method onto `LifecycleNotifier`, whose
public surface otherwise means "fire a host callback".

Split: `stopping()` answers only what a cancellation is about to stop, and
`LifecycleNotifier.cancelling()` owns the try/catch and the log. `cancelled()` is private
now, since nothing outside the class fires a single one.

Also drops `@since` from `QueuedJob`: it is not re-exported from `src/index.ts`, so the
package rule does not reach it, and its neighbour `CollectionStatusItem` carries none.

This branch was successfully deployed

1 active deployment
Preview — fe027e5b Deployed Oct 6, 2026 by vercel[bot]
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.

1 participant