@purposeinplay/payload-ai-translate 0.1.0 → 0.1.1
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.
|
@@ -134,7 +134,11 @@ export async function runWorkerForUnit(params) {
|
|
|
134
134
|
// batches: a janitor-reset unit whose batch already ended must not
|
|
135
135
|
// run again.
|
|
136
136
|
// -------------------------------------------------------------------
|
|
137
|
-
if (batchStatus === 'cancelling' || batchStatus === 'cancelled' || batchStatus === 'reverted') {
|
|
137
|
+
if (batchStatus === 'cancelling' || batchStatus === 'cancelled' || batchStatus === 'reverted' || batchStatus === 'success' || batchStatus === 'failed' || batchStatus === 'partial') {
|
|
138
|
+
// Preserve the existing `cancelled_by_admin` reason for the cancel /
|
|
139
|
+
// revert path (callers + tests rely on it); a batch that finished on
|
|
140
|
+
// its own (`success`/`failed`/`partial`) gets a distinct reason.
|
|
141
|
+
const skipReason = batchStatus === 'success' || batchStatus === 'failed' || batchStatus === 'partial' ? 'batch_already_terminal' : 'cancelled_by_admin';
|
|
138
142
|
// Only rewrite units that are still open — a terminal unit (e.g.
|
|
139
143
|
// `failed`) re-fired by a duplicate job must keep its real outcome.
|
|
140
144
|
if (unit.status === 'pending' || unit.status === 'running') {
|
|
@@ -143,7 +147,7 @@ export async function runWorkerForUnit(params) {
|
|
|
143
147
|
id: unitId,
|
|
144
148
|
data: {
|
|
145
149
|
status: 'skipped',
|
|
146
|
-
failureMessage:
|
|
150
|
+
failureMessage: skipReason,
|
|
147
151
|
completedAt: now().toISOString()
|
|
148
152
|
},
|
|
149
153
|
overrideAccess: true
|
|
@@ -155,7 +159,7 @@ export async function runWorkerForUnit(params) {
|
|
|
155
159
|
await maybeTransitionBatch(payload, batchesSlug, unitsSlug, unit.batchId, callbacks);
|
|
156
160
|
return {
|
|
157
161
|
status: 'skipped',
|
|
158
|
-
failureMessage:
|
|
162
|
+
failureMessage: skipReason
|
|
159
163
|
};
|
|
160
164
|
}
|
|
161
165
|
// -------------------------------------------------------------------
|
|
@@ -242,11 +246,38 @@ export async function runWorkerForUnit(params) {
|
|
|
242
246
|
documentId: unit.documentId
|
|
243
247
|
});
|
|
244
248
|
if (claim.acquired === false) {
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
249
|
+
// Two very different failure modes, previously conflated into an
|
|
250
|
+
// unconditional re-enqueue — which is what caused the 2026-06-16
|
|
251
|
+
// outage: a cancelled/finished batch's leftover jobs re-enqueued
|
|
252
|
+
// themselves every cron tick forever, each re-reading the source doc.
|
|
253
|
+
//
|
|
254
|
+
// - `not_pending`: this unit is no longer `pending` (it's terminal,
|
|
255
|
+
// or another fire of the SAME job claimed it). There is no work to
|
|
256
|
+
// retry. Complete the job WITHOUT re-enqueuing — re-enqueuing here
|
|
257
|
+
// is the infinite loop. Return the unit's real current status so
|
|
258
|
+
// the job output is truthful; never write the unit row (a running
|
|
259
|
+
// sibling fire owns its outcome).
|
|
260
|
+
// - `sibling_running`: a DIFFERENT locale of the same doc is running
|
|
261
|
+
// and this unit is still `pending` and genuinely needs to retry on
|
|
262
|
+
// a later tick. This is the only case that warrants a re-enqueue.
|
|
263
|
+
if (claim.reason === 'not_pending') {
|
|
264
|
+
const fresh = await payload.findByID({
|
|
265
|
+
collection: unitsSlug,
|
|
266
|
+
id: unitId,
|
|
267
|
+
overrideAccess: true,
|
|
268
|
+
depth: 0
|
|
269
|
+
});
|
|
270
|
+
const freshStatus = fresh?.status;
|
|
271
|
+
const mapped = freshStatus === 'success' ? 'success' : freshStatus === 'failed' ? 'failed' : 'skipped';
|
|
272
|
+
earlyLog.event('info', 'bulk.worker.unit.terminal-unit-no-requeue', {
|
|
273
|
+
unitStatus: freshStatus ?? 'unknown'
|
|
274
|
+
});
|
|
275
|
+
return {
|
|
276
|
+
status: mapped
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
// sibling_running — re-enqueue this unit so a later cron tick retries;
|
|
280
|
+
// don't bump attempts (deferral isn't a translation failure).
|
|
250
281
|
try {
|
|
251
282
|
await payload.jobs.queue({
|
|
252
283
|
task: BULK_TRANSLATE_DOC_TASK_SLUG,
|
|
@@ -85,12 +85,72 @@ export async function runJanitorSweep(params) {
|
|
|
85
85
|
const stale = result.docs;
|
|
86
86
|
const batchesSlug = params.batchesSlug ?? DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG;
|
|
87
87
|
const affectedBatchIds = new Set();
|
|
88
|
-
const
|
|
88
|
+
const extractBatchId = (u)=>{
|
|
89
89
|
const raw = typeof u.batchId === 'object' && u.batchId !== null ? u.batchId.id : u.batchId;
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
return raw !== undefined && raw !== null && String(raw).length > 0 ? String(raw) : null;
|
|
91
|
+
};
|
|
92
|
+
const noteBatch = (u)=>{
|
|
93
|
+
const id = extractBatchId(u);
|
|
94
|
+
if (id) {
|
|
95
|
+
affectedBatchIds.add(id);
|
|
92
96
|
}
|
|
93
97
|
};
|
|
98
|
+
// A batch in a terminal state must never have its units re-queued: the
|
|
99
|
+
// run is over, and a re-queued job would only re-read the source doc
|
|
100
|
+
// before being skipped. Re-queueing terminal-batch units (left behind by
|
|
101
|
+
// a cancel/finish) is what produced the 2026-06-16 zombie-job storm.
|
|
102
|
+
// `queued`/`running`/`cancelling` are still live and safe to re-queue
|
|
103
|
+
// into (the worker's cancel gate skips cancelling units on entry).
|
|
104
|
+
const REQUEUEABLE_BATCH_STATUSES = new Set([
|
|
105
|
+
'queued',
|
|
106
|
+
'running',
|
|
107
|
+
'cancelling'
|
|
108
|
+
]);
|
|
109
|
+
const batchStatusCache = new Map();
|
|
110
|
+
const getBatchStatus = async (batchId)=>{
|
|
111
|
+
if (batchStatusCache.has(batchId)) {
|
|
112
|
+
return batchStatusCache.get(batchId);
|
|
113
|
+
}
|
|
114
|
+
let status;
|
|
115
|
+
try {
|
|
116
|
+
const batch = await payload.findByID({
|
|
117
|
+
collection: batchesSlug,
|
|
118
|
+
id: batchId,
|
|
119
|
+
overrideAccess: true,
|
|
120
|
+
depth: 0
|
|
121
|
+
});
|
|
122
|
+
status = batch?.status;
|
|
123
|
+
} catch {
|
|
124
|
+
status = undefined;
|
|
125
|
+
}
|
|
126
|
+
batchStatusCache.set(batchId, status);
|
|
127
|
+
return status;
|
|
128
|
+
};
|
|
129
|
+
// Whether a unit's batch is still live enough to re-queue into. Tolerant
|
|
130
|
+
// on a missing/unreadable batch (undefined) — preserve prior behaviour
|
|
131
|
+
// and let the worker's own guards handle it.
|
|
132
|
+
const batchIsRequeueable = async (u)=>{
|
|
133
|
+
const batchId = extractBatchId(u);
|
|
134
|
+
if (!batchId) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
const status = await getBatchStatus(batchId);
|
|
138
|
+
return status === undefined || REQUEUEABLE_BATCH_STATUSES.has(status);
|
|
139
|
+
};
|
|
140
|
+
// Mark a unit terminal (skipped) so a dead/terminal-batch orphan stops
|
|
141
|
+
// being rescanned every sweep, without creating a replacement job.
|
|
142
|
+
const skipTerminalBatchUnit = async (unitId)=>{
|
|
143
|
+
await payload.update({
|
|
144
|
+
collection: unitsSlug,
|
|
145
|
+
id: unitId,
|
|
146
|
+
data: {
|
|
147
|
+
status: 'skipped',
|
|
148
|
+
failureMessage: 'batch_already_terminal',
|
|
149
|
+
completedAt: new Date(now()).toISOString()
|
|
150
|
+
},
|
|
151
|
+
overrideAccess: true
|
|
152
|
+
});
|
|
153
|
+
};
|
|
94
154
|
// A unit's worker job is queued exactly once (at enumeration, or by the
|
|
95
155
|
// worker's own deferral re-queue). The worker task has `retries: 0`, and
|
|
96
156
|
// the coordinator never re-runs after enumeration finishes — so a reset
|
|
@@ -111,6 +171,12 @@ export async function runJanitorSweep(params) {
|
|
|
111
171
|
for (const u of stale){
|
|
112
172
|
const attempts = u.attempts ?? 0;
|
|
113
173
|
try {
|
|
174
|
+
// Don't resurrect a unit whose batch already ended — mark it
|
|
175
|
+
// terminal instead so it leaves the stale scan without spawning a job.
|
|
176
|
+
if (!await batchIsRequeueable(u)) {
|
|
177
|
+
await skipTerminalBatchUnit(u.id);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
114
180
|
if (attempts < maxAttempts) {
|
|
115
181
|
await payload.update({
|
|
116
182
|
collection: unitsSlug,
|
|
@@ -205,6 +271,12 @@ export async function runJanitorSweep(params) {
|
|
|
205
271
|
});
|
|
206
272
|
for (const u of orphans.docs){
|
|
207
273
|
try {
|
|
274
|
+
// Skip — don't re-queue — units whose batch already ended; mark
|
|
275
|
+
// them terminal so they drop out of the orphan scan.
|
|
276
|
+
if (!await batchIsRequeueable(u)) {
|
|
277
|
+
await skipTerminalBatchUnit(u.id);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
208
280
|
await requeueUnit(u.id);
|
|
209
281
|
await payload.update({
|
|
210
282
|
collection: unitsSlug,
|
package/package.json
CHANGED
|
@@ -1,46 +1,98 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@purposeinplay/payload-ai-translate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "AI translation plugin for Payload CMS 3 — multi-provider (OpenAI, Anthropic, Gemini, custom), bulk translation, Lexical-aware, with an admin Translation Hub.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
-
"
|
|
8
|
-
"
|
|
7
|
+
"packageManager": "pnpm@9.15.0",
|
|
8
|
+
"main": "./src/exports/index.ts",
|
|
9
|
+
"types": "./src/exports/index.ts",
|
|
9
10
|
"exports": {
|
|
10
11
|
".": {
|
|
11
|
-
"types": "./
|
|
12
|
-
"import": "./
|
|
13
|
-
"default": "./
|
|
12
|
+
"types": "./src/exports/index.ts",
|
|
13
|
+
"import": "./src/exports/index.ts",
|
|
14
|
+
"default": "./src/exports/index.ts"
|
|
14
15
|
},
|
|
15
16
|
"./providers": {
|
|
16
|
-
"types": "./
|
|
17
|
-
"import": "./
|
|
18
|
-
"default": "./
|
|
17
|
+
"types": "./src/exports/providers.ts",
|
|
18
|
+
"import": "./src/exports/providers.ts",
|
|
19
|
+
"default": "./src/exports/providers.ts"
|
|
19
20
|
},
|
|
20
21
|
"./views": {
|
|
21
|
-
"types": "./
|
|
22
|
-
"import": "./
|
|
23
|
-
"default": "./
|
|
22
|
+
"types": "./src/exports/views.ts",
|
|
23
|
+
"import": "./src/exports/views.ts",
|
|
24
|
+
"default": "./src/exports/views.ts"
|
|
24
25
|
},
|
|
25
26
|
"./views-client": {
|
|
26
|
-
"types": "./
|
|
27
|
-
"import": "./
|
|
28
|
-
"default": "./
|
|
27
|
+
"types": "./src/exports/views-client.ts",
|
|
28
|
+
"import": "./src/exports/views-client.ts",
|
|
29
|
+
"default": "./src/exports/views-client.ts"
|
|
29
30
|
},
|
|
30
31
|
"./client": {
|
|
31
|
-
"types": "./
|
|
32
|
-
"import": "./
|
|
33
|
-
"default": "./
|
|
32
|
+
"types": "./src/exports/client.ts",
|
|
33
|
+
"import": "./src/exports/client.ts",
|
|
34
|
+
"default": "./src/exports/client.ts"
|
|
34
35
|
},
|
|
35
36
|
"./components": {
|
|
36
|
-
"types": "./
|
|
37
|
-
"import": "./
|
|
38
|
-
"default": "./
|
|
37
|
+
"types": "./src/exports/components.ts",
|
|
38
|
+
"import": "./src/exports/components.ts",
|
|
39
|
+
"default": "./src/exports/components.ts"
|
|
39
40
|
}
|
|
40
41
|
},
|
|
41
42
|
"files": [
|
|
42
43
|
"dist"
|
|
43
44
|
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"main": "./dist/exports/index.js",
|
|
47
|
+
"types": "./dist/exports/index.d.ts",
|
|
48
|
+
"exports": {
|
|
49
|
+
".": {
|
|
50
|
+
"types": "./dist/exports/index.d.ts",
|
|
51
|
+
"import": "./dist/exports/index.js",
|
|
52
|
+
"default": "./dist/exports/index.js"
|
|
53
|
+
},
|
|
54
|
+
"./providers": {
|
|
55
|
+
"types": "./dist/exports/providers.d.ts",
|
|
56
|
+
"import": "./dist/exports/providers.js",
|
|
57
|
+
"default": "./dist/exports/providers.js"
|
|
58
|
+
},
|
|
59
|
+
"./views": {
|
|
60
|
+
"types": "./dist/exports/views.d.ts",
|
|
61
|
+
"import": "./dist/exports/views.js",
|
|
62
|
+
"default": "./dist/exports/views.js"
|
|
63
|
+
},
|
|
64
|
+
"./views-client": {
|
|
65
|
+
"types": "./dist/exports/views-client.d.ts",
|
|
66
|
+
"import": "./dist/exports/views-client.js",
|
|
67
|
+
"default": "./dist/exports/views-client.js"
|
|
68
|
+
},
|
|
69
|
+
"./client": {
|
|
70
|
+
"types": "./dist/exports/client.d.ts",
|
|
71
|
+
"import": "./dist/exports/client.js",
|
|
72
|
+
"default": "./dist/exports/client.js"
|
|
73
|
+
},
|
|
74
|
+
"./components": {
|
|
75
|
+
"types": "./dist/exports/components.d.ts",
|
|
76
|
+
"import": "./dist/exports/components.js",
|
|
77
|
+
"default": "./dist/exports/components.js"
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"scripts": {
|
|
82
|
+
"build": "pnpm run build:types && pnpm run build:swc && pnpm run build:fix-esm",
|
|
83
|
+
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
84
|
+
"build:swc": "swc ./src -d ./dist --config-file ./.swcrc --strip-leading-paths --ignore \"**/__tests__/**\"",
|
|
85
|
+
"build:fix-esm": "node ./scripts/fix-dist-extensions.mjs dist && node ./scripts/check-dist-esm.mjs dist",
|
|
86
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
87
|
+
"test": "vitest run",
|
|
88
|
+
"lint": "biome check .",
|
|
89
|
+
"format": "biome format --write .",
|
|
90
|
+
"check": "biome check . && pnpm typecheck && pnpm build && pnpm test",
|
|
91
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
92
|
+
"changeset": "changeset",
|
|
93
|
+
"version-packages": "changeset version",
|
|
94
|
+
"release": "pnpm build && changeset publish"
|
|
95
|
+
},
|
|
44
96
|
"peerDependencies": {
|
|
45
97
|
"@ai-sdk/anthropic": "^3.0.0",
|
|
46
98
|
"@ai-sdk/google": "^3.0.0",
|
|
@@ -122,20 +174,5 @@
|
|
|
122
174
|
"openai",
|
|
123
175
|
"anthropic",
|
|
124
176
|
"gemini"
|
|
125
|
-
]
|
|
126
|
-
|
|
127
|
-
"build": "pnpm run build:types && pnpm run build:swc && pnpm run build:fix-esm",
|
|
128
|
-
"build:types": "tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist",
|
|
129
|
-
"build:swc": "swc ./src -d ./dist --config-file ./.swcrc --strip-leading-paths --ignore \"**/__tests__/**\"",
|
|
130
|
-
"build:fix-esm": "node ./scripts/fix-dist-extensions.mjs dist && node ./scripts/check-dist-esm.mjs dist",
|
|
131
|
-
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
132
|
-
"test": "vitest run",
|
|
133
|
-
"lint": "biome check .",
|
|
134
|
-
"format": "biome format --write .",
|
|
135
|
-
"check": "biome check . && pnpm typecheck && pnpm build && pnpm test",
|
|
136
|
-
"clean": "rm -rf dist *.tsbuildinfo",
|
|
137
|
-
"changeset": "changeset",
|
|
138
|
-
"version-packages": "changeset version",
|
|
139
|
-
"release": "pnpm build && changeset publish"
|
|
140
|
-
}
|
|
141
|
-
}
|
|
177
|
+
]
|
|
178
|
+
}
|