@focus-reactive/payload-plugin-translator 0.3.0 → 0.5.0
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/README.md +44 -29
- package/dist/index.d.ts +16 -14
- package/dist/index.js +9 -7
- package/dist/plugin.d.ts +15 -4
- package/dist/plugin.js +34 -57
- package/dist/server/features/cancel/handler.d.ts +3 -3
- package/dist/server/features/cancel/handler.js +5 -3
- package/dist/server/features/cancel/route.d.ts +4 -4
- package/dist/server/features/cancel/route.js +4 -4
- package/dist/server/features/cancel-by-collection/handler.d.ts +4 -4
- package/dist/server/features/cancel-by-collection/handler.js +6 -6
- package/dist/server/features/cancel-by-collection/route.d.ts +5 -5
- package/dist/server/features/cancel-by-collection/route.js +4 -4
- package/dist/server/features/createTranslationRoutes.d.ts +23 -0
- package/dist/server/features/createTranslationRoutes.js +27 -0
- package/dist/server/features/enqueue-translation/handler.d.ts +4 -4
- package/dist/server/features/enqueue-translation/handler.js +5 -5
- package/dist/server/features/enqueue-translation/route.d.ts +5 -5
- package/dist/server/features/enqueue-translation/route.js +4 -4
- package/dist/server/features/get-collection-status/handler.d.ts +4 -4
- package/dist/server/features/get-collection-status/handler.js +5 -5
- package/dist/server/features/get-collection-status/route.d.ts +5 -5
- package/dist/server/features/get-collection-status/route.js +4 -4
- package/dist/server/features/get-document-status/handler.d.ts +4 -4
- package/dist/server/features/get-document-status/handler.js +5 -5
- package/dist/server/features/get-document-status/route.d.ts +5 -5
- package/dist/server/features/get-document-status/route.js +4 -4
- package/dist/server/features/index.d.ts +7 -6
- package/dist/server/features/index.js +7 -6
- package/dist/server/features/run-translation/handler.d.ts +3 -3
- package/dist/server/features/run-translation/handler.js +7 -7
- package/dist/server/features/run-translation/route.d.ts +4 -4
- package/dist/server/features/run-translation/route.js +4 -4
- package/dist/server/features/translate-document/handler.js +7 -11
- package/dist/server/modules/task-runner/TaskRunnerProvider.interface.d.ts +19 -1
- package/dist/server/modules/task-runner/index.d.ts +6 -6
- package/dist/server/modules/task-runner/index.js +2 -2
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.d.ts +16 -1
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.js +42 -1
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.d.ts +29 -0
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.js +101 -6
- package/dist/server/modules/task-runner/payload-jobs-runner/types.d.ts +17 -0
- package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.d.ts +18 -10
- package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.js +24 -14
- package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +46 -0
- package/dist/server/modules/translation-levels/PluginConfigBuilder.js +108 -0
- package/dist/server/modules/translation-levels/collectionLevel.d.ts +15 -0
- package/dist/server/modules/translation-levels/collectionLevel.js +24 -0
- package/dist/server/modules/translation-levels/documentLevel.d.ts +19 -0
- package/dist/server/modules/translation-levels/documentLevel.js +28 -0
- package/dist/server/modules/translation-levels/index.d.ts +3 -0
- package/dist/server/modules/translation-levels/index.js +4 -0
- package/dist/server/modules/translation-levels/types.d.ts +42 -0
- package/dist/server/modules/translation-levels/types.js +10 -0
- package/dist/server/modules/translation-levels/useDocTranslationApi.d.ts +8 -0
- package/dist/server/modules/translation-levels/useDocTranslationApi.js +18 -0
- package/dist/server/modules/translation-pipeline/index.d.ts +5 -3
- package/dist/server/modules/translation-pipeline/index.js +3 -2
- package/dist/server/modules/translation-pipeline/translateContent.d.ts +35 -0
- package/dist/server/modules/translation-pipeline/translateContent.js +30 -0
- package/dist/server/modules/translation-providers/OpenAITranslation.provider.d.ts +4 -3
- package/dist/server/modules/translation-providers/OpenAITranslation.provider.js +15 -15
- package/dist/types/AccessGuard.d.ts +24 -1
- package/dist/types/AccessGuard.js +12 -1
- package/package.json +1 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { PayloadRequest } from
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
1
|
+
import type { PayloadRequest } from "payload";
|
|
2
|
+
import type { TaskRunnerFactory } from "../../modules/task-runner";
|
|
3
|
+
import type { GetCollectionStatusConfig } from "./model";
|
|
4
4
|
/**
|
|
5
5
|
* Gets translation status for all documents in a collection
|
|
6
6
|
*/
|
|
7
7
|
export declare class GetCollectionStatusHandler {
|
|
8
8
|
private readonly config;
|
|
9
9
|
private readonly taskRunnerFactory;
|
|
10
|
-
constructor(config: GetCollectionStatusConfig, taskRunnerFactory:
|
|
10
|
+
constructor(config: GetCollectionStatusConfig, taskRunnerFactory: TaskRunnerFactory);
|
|
11
11
|
handle(req: PayloadRequest): Promise<Response>;
|
|
12
12
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ServerResponse } from
|
|
2
|
-
import { isCollectionAvailable } from
|
|
3
|
-
import { GetCollectionStatusInputSchema } from
|
|
1
|
+
import { ServerResponse } from "../../shared";
|
|
2
|
+
import { isCollectionAvailable } from "../_lib/collection-utils";
|
|
3
|
+
import { GetCollectionStatusInputSchema } from "./model";
|
|
4
4
|
/**
|
|
5
5
|
* Gets translation status for all documents in a collection
|
|
6
6
|
*/ export class GetCollectionStatusHandler {
|
|
@@ -12,9 +12,9 @@ import { GetCollectionStatusInputSchema } from './model';
|
|
|
12
12
|
}
|
|
13
13
|
async handle(req) {
|
|
14
14
|
const validationResult = GetCollectionStatusInputSchema.safeParse(req.routeParams);
|
|
15
|
-
if (validationResult.error) return ServerResponse.validationError(validationResult.error.
|
|
15
|
+
if (validationResult.error) return ServerResponse.validationError(validationResult.error.issues);
|
|
16
16
|
const collectionSlug = isCollectionAvailable(validationResult.data.collection_slug, this.config.availableCollections);
|
|
17
|
-
if (!collectionSlug) return ServerResponse.badRequest(
|
|
17
|
+
if (!collectionSlug) return ServerResponse.badRequest("Collection not available for translation");
|
|
18
18
|
const runner = this.taskRunnerFactory.create(req.payload);
|
|
19
19
|
const tasks = await runner.findByCollection(collectionSlug);
|
|
20
20
|
return ServerResponse.success({
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Endpoint } from
|
|
2
|
-
import type { AccessGuard } from
|
|
3
|
-
import type {
|
|
4
|
-
import type { GetCollectionStatusConfig } from
|
|
1
|
+
import type { Endpoint } from "payload";
|
|
2
|
+
import type { AccessGuard } from "../../shared";
|
|
3
|
+
import type { TaskRunnerFactory } from "../../modules/task-runner";
|
|
4
|
+
import type { GetCollectionStatusConfig } from "./model";
|
|
5
5
|
/**
|
|
6
6
|
* Creates the get collection status endpoint
|
|
7
7
|
*/
|
|
8
|
-
export declare function createGetCollectionStatusRoute(config: GetCollectionStatusConfig, taskRunnerFactory:
|
|
8
|
+
export declare function createGetCollectionStatusRoute(config: GetCollectionStatusConfig, taskRunnerFactory: TaskRunnerFactory, access?: AccessGuard, basePath?: string): Endpoint;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { withErrorHandler, withAccessCheck } from
|
|
2
|
-
import { GetCollectionStatusHandler } from
|
|
1
|
+
import { withErrorHandler, withAccessCheck } from "../../shared";
|
|
2
|
+
import { GetCollectionStatusHandler } from "./handler";
|
|
3
3
|
/**
|
|
4
4
|
* Creates the get collection status endpoint
|
|
5
|
-
*/ export function createGetCollectionStatusRoute(config, taskRunnerFactory, access, basePath =
|
|
5
|
+
*/ export function createGetCollectionStatusRoute(config, taskRunnerFactory, access, basePath = "/translate") {
|
|
6
6
|
const handler = new GetCollectionStatusHandler(config, taskRunnerFactory);
|
|
7
7
|
return {
|
|
8
8
|
path: `${basePath}/collection/:collection_slug`,
|
|
9
|
-
method:
|
|
9
|
+
method: "get",
|
|
10
10
|
handler: withAccessCheck(withErrorHandler(handler.handle.bind(handler)), access)
|
|
11
11
|
};
|
|
12
12
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import type { PayloadRequest } from
|
|
2
|
-
import type {
|
|
3
|
-
import {
|
|
1
|
+
import type { PayloadRequest } from "payload";
|
|
2
|
+
import type { TaskRunnerFactory } from "../../modules/task-runner";
|
|
3
|
+
import type { GetDocumentStatusConfig } from "./model";
|
|
4
4
|
/**
|
|
5
5
|
* Gets the translation status for a specific document
|
|
6
6
|
*/
|
|
7
7
|
export declare class GetDocumentStatusHandler {
|
|
8
8
|
private readonly config;
|
|
9
9
|
private readonly taskRunnerFactory;
|
|
10
|
-
constructor(config: GetDocumentStatusConfig, taskRunnerFactory:
|
|
10
|
+
constructor(config: GetDocumentStatusConfig, taskRunnerFactory: TaskRunnerFactory);
|
|
11
11
|
handle(req: PayloadRequest): Promise<Response>;
|
|
12
12
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ServerResponse } from
|
|
2
|
-
import { isCollectionAvailable } from
|
|
3
|
-
import { GetDocumentStatusInputSchema, taskToJobStatusOutput } from
|
|
1
|
+
import { ServerResponse } from "../../shared";
|
|
2
|
+
import { isCollectionAvailable } from "../_lib/collection-utils";
|
|
3
|
+
import { GetDocumentStatusInputSchema, taskToJobStatusOutput } from "./model";
|
|
4
4
|
/**
|
|
5
5
|
* Gets the translation status for a specific document
|
|
6
6
|
*/ export class GetDocumentStatusHandler {
|
|
@@ -12,10 +12,10 @@ import { GetDocumentStatusInputSchema, taskToJobStatusOutput } from './model';
|
|
|
12
12
|
}
|
|
13
13
|
async handle(req) {
|
|
14
14
|
const validationResult = GetDocumentStatusInputSchema.safeParse(req.routeParams);
|
|
15
|
-
if (validationResult.error) return ServerResponse.validationError(validationResult.error.
|
|
15
|
+
if (validationResult.error) return ServerResponse.validationError(validationResult.error.issues);
|
|
16
16
|
const { collection_slug, collection_id } = validationResult.data;
|
|
17
17
|
const collectionSlug = isCollectionAvailable(collection_slug, this.config.availableCollections);
|
|
18
|
-
if (!collectionSlug) return ServerResponse.badRequest(
|
|
18
|
+
if (!collectionSlug) return ServerResponse.badRequest("Collection not available for translation");
|
|
19
19
|
const runner = this.taskRunnerFactory.create(req.payload);
|
|
20
20
|
const tasks = await runner.findByCollection(collectionSlug, [
|
|
21
21
|
collection_id
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { Endpoint } from
|
|
2
|
-
import type { AccessGuard } from
|
|
3
|
-
import type {
|
|
4
|
-
import type { GetDocumentStatusConfig } from
|
|
1
|
+
import type { Endpoint } from "payload";
|
|
2
|
+
import type { AccessGuard } from "../../shared";
|
|
3
|
+
import type { TaskRunnerFactory } from "../../modules/task-runner";
|
|
4
|
+
import type { GetDocumentStatusConfig } from "./model";
|
|
5
5
|
/**
|
|
6
6
|
* Creates the get document status endpoint
|
|
7
7
|
*/
|
|
8
|
-
export declare function createGetDocumentStatusRoute(config: GetDocumentStatusConfig, taskRunnerFactory:
|
|
8
|
+
export declare function createGetDocumentStatusRoute(config: GetDocumentStatusConfig, taskRunnerFactory: TaskRunnerFactory, access?: AccessGuard, basePath?: string): Endpoint;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { withErrorHandler, withAccessCheck } from
|
|
2
|
-
import { GetDocumentStatusHandler } from
|
|
1
|
+
import { withErrorHandler, withAccessCheck } from "../../shared";
|
|
2
|
+
import { GetDocumentStatusHandler } from "./handler";
|
|
3
3
|
/**
|
|
4
4
|
* Creates the get document status endpoint
|
|
5
|
-
*/ export function createGetDocumentStatusRoute(config, taskRunnerFactory, access, basePath =
|
|
5
|
+
*/ export function createGetDocumentStatusRoute(config, taskRunnerFactory, access, basePath = "/translate") {
|
|
6
6
|
const handler = new GetDocumentStatusHandler(config, taskRunnerFactory);
|
|
7
7
|
return {
|
|
8
8
|
path: `${basePath}/document/:collection_slug/:collection_id`,
|
|
9
|
-
method:
|
|
9
|
+
method: "get",
|
|
10
10
|
handler: withAccessCheck(withErrorHandler(handler.handle.bind(handler)), access)
|
|
11
11
|
};
|
|
12
12
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
1
|
+
export * from "./createTranslationRoutes";
|
|
2
|
+
export * from "./translate-document";
|
|
3
|
+
export * from "./enqueue-translation";
|
|
4
|
+
export * from "./run-translation";
|
|
5
|
+
export * from "./cancel";
|
|
6
|
+
export * from "./get-document-status";
|
|
7
|
+
export * from "./get-collection-status";
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
export * from
|
|
2
|
-
export * from
|
|
3
|
-
export * from
|
|
4
|
-
export * from
|
|
5
|
-
export * from
|
|
6
|
-
export * from
|
|
1
|
+
export * from "./createTranslationRoutes";
|
|
2
|
+
export * from "./translate-document";
|
|
3
|
+
export * from "./enqueue-translation";
|
|
4
|
+
export * from "./run-translation";
|
|
5
|
+
export * from "./cancel";
|
|
6
|
+
export * from "./get-document-status";
|
|
7
|
+
export * from "./get-collection-status";
|
|
7
8
|
|
|
8
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { PayloadRequest } from
|
|
2
|
-
import type {
|
|
1
|
+
import type { PayloadRequest } from "payload";
|
|
2
|
+
import type { TaskRunnerFactory } from "../../modules/task-runner";
|
|
3
3
|
/**
|
|
4
4
|
* Runs a translation task by ID
|
|
5
5
|
*/
|
|
6
6
|
export declare class RunTranslationHandler {
|
|
7
7
|
private readonly taskRunnerFactory;
|
|
8
|
-
constructor(taskRunnerFactory:
|
|
8
|
+
constructor(taskRunnerFactory: TaskRunnerFactory);
|
|
9
9
|
handle(req: PayloadRequest): Promise<Response>;
|
|
10
10
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ServerResponse } from
|
|
2
|
-
import { RunInputSchema } from
|
|
1
|
+
import { ServerResponse } from "../../shared";
|
|
2
|
+
import { RunInputSchema } from "./model";
|
|
3
3
|
/**
|
|
4
4
|
* Runs a translation task by ID
|
|
5
5
|
*/ export class RunTranslationHandler {
|
|
@@ -9,16 +9,16 @@ import { RunInputSchema } from './model';
|
|
|
9
9
|
}
|
|
10
10
|
async handle(req) {
|
|
11
11
|
const validationResult = RunInputSchema.safeParse(req.routeParams);
|
|
12
|
-
if (validationResult.error) return ServerResponse.validationError(validationResult.error.
|
|
12
|
+
if (validationResult.error) return ServerResponse.validationError(validationResult.error.issues);
|
|
13
13
|
const { id } = validationResult.data;
|
|
14
14
|
const runner = this.taskRunnerFactory.create(req.payload);
|
|
15
15
|
const result = await runner.run(id);
|
|
16
16
|
if (!result.success) {
|
|
17
|
-
if (result.error ===
|
|
18
|
-
return ServerResponse.notFound(
|
|
17
|
+
if (result.error === "not_found" || result.error === "already_completed") {
|
|
18
|
+
return ServerResponse.notFound("Queued task not found");
|
|
19
19
|
}
|
|
20
|
-
if (result.error ===
|
|
21
|
-
return ServerResponse.tooManyRequests(
|
|
20
|
+
if (result.error === "already_running") {
|
|
21
|
+
return ServerResponse.tooManyRequests("Translation task is already in progress");
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
return ServerResponse.noContent();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Endpoint } from
|
|
2
|
-
import type { AccessGuard } from
|
|
3
|
-
import type {
|
|
1
|
+
import type { Endpoint } from "payload";
|
|
2
|
+
import type { AccessGuard } from "../../shared";
|
|
3
|
+
import type { TaskRunnerFactory } from "../../modules/task-runner";
|
|
4
4
|
/**
|
|
5
5
|
* Creates the run translation endpoint
|
|
6
6
|
*/
|
|
7
|
-
export declare function createRunRoute(taskRunnerFactory:
|
|
7
|
+
export declare function createRunRoute(taskRunnerFactory: TaskRunnerFactory, access?: AccessGuard, basePath?: string): Endpoint;
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { withErrorHandler, withAccessCheck } from
|
|
2
|
-
import { RunTranslationHandler } from
|
|
1
|
+
import { withErrorHandler, withAccessCheck } from "../../shared";
|
|
2
|
+
import { RunTranslationHandler } from "./handler";
|
|
3
3
|
/**
|
|
4
4
|
* Creates the run translation endpoint
|
|
5
|
-
*/ export function createRunRoute(taskRunnerFactory, access, basePath =
|
|
5
|
+
*/ export function createRunRoute(taskRunnerFactory, access, basePath = "/translate") {
|
|
6
6
|
const handler = new RunTranslationHandler(taskRunnerFactory);
|
|
7
7
|
return {
|
|
8
8
|
path: `${basePath}/run/:id`,
|
|
9
|
-
method:
|
|
9
|
+
method: "post",
|
|
10
10
|
handler: withAccessCheck(withErrorHandler(handler.handle.bind(handler)), access)
|
|
11
11
|
};
|
|
12
12
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { APIError } from "payload";
|
|
2
|
-
import {
|
|
3
|
-
import { createTranslationStrategy } from "../../modules/translation-pipeline/strategies";
|
|
2
|
+
import { translateContent } from "../../modules/translation-pipeline";
|
|
4
3
|
/**
|
|
5
4
|
* Translates a single document from source language to target language
|
|
6
5
|
*/ export class TranslateDocumentHandler {
|
|
@@ -28,23 +27,20 @@ import { createTranslationStrategy } from "../../modules/translation-pipeline/st
|
|
|
28
27
|
fallbackLocale: false,
|
|
29
28
|
depth: 0
|
|
30
29
|
});
|
|
31
|
-
const
|
|
32
|
-
const pipeline = new TranslationPipeline({
|
|
33
|
-
translationProvider: this.translationProvider,
|
|
34
|
-
translationStrategy
|
|
35
|
-
});
|
|
36
|
-
const result = await pipeline.execute({
|
|
30
|
+
const translatedData = await translateContent({
|
|
37
31
|
schema,
|
|
38
32
|
sourceData,
|
|
39
33
|
targetData,
|
|
40
34
|
sourceLng,
|
|
41
|
-
targetLng
|
|
35
|
+
targetLng,
|
|
36
|
+
translationProvider: this.translationProvider,
|
|
37
|
+
strategy
|
|
42
38
|
});
|
|
43
|
-
if (!
|
|
39
|
+
if (!translatedData) return {
|
|
44
40
|
success: true
|
|
45
41
|
};
|
|
46
42
|
const collectionConfig = payload.collections[collection].config;
|
|
47
|
-
await this.saveTranslatedDocument(payload, collection, collectionId,
|
|
43
|
+
await this.saveTranslatedDocument(payload, collection, collectionId, translatedData, targetLng, sourceLng, collectionConfig, publishOnTranslation);
|
|
48
44
|
return {
|
|
49
45
|
success: true
|
|
50
46
|
};
|
|
@@ -24,6 +24,16 @@ export type TaskRunnerContext = {
|
|
|
24
24
|
handler: TaskHandler;
|
|
25
25
|
collections: CollectionSlug[];
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* The runtime half of a runner: produces a `TaskRunner` for one request.
|
|
29
|
+
*
|
|
30
|
+
* Routes depend on this narrow surface — they only ever `create()`, never
|
|
31
|
+
* `configure()`. The plugin binds the {@link TaskRunnerContext} once at build
|
|
32
|
+
* time and hands routes a factory, so a `create()` call needs no ambient state.
|
|
33
|
+
*/
|
|
34
|
+
export type TaskRunnerFactory = {
|
|
35
|
+
create(payload: Payload): TaskRunner;
|
|
36
|
+
};
|
|
27
37
|
/**
|
|
28
38
|
* Main interface for pluggable task runner providers.
|
|
29
39
|
*
|
|
@@ -36,11 +46,19 @@ export type TaskRunnerContext = {
|
|
|
36
46
|
export interface TaskRunnerProvider {
|
|
37
47
|
/**
|
|
38
48
|
* Creates a TaskRunner instance for runtime operations (enqueue, cancel, find).
|
|
49
|
+
*
|
|
50
|
+
* `handler` is supplied by the caller at create time (the plugin binds it
|
|
51
|
+
* into a {@link TaskRunnerFactory}) — so providers hold no mutable per-instance
|
|
52
|
+
* state and `create()` does not depend on `configure()` having run. It is the
|
|
53
|
+
* only runtime dependency (collection metadata is a `configure()`-time concern).
|
|
54
|
+
* `PayloadJobsRunner` ignores it (the handler is baked into the registered
|
|
55
|
+
* task at configure time); `SyncRunner` uses it to run translations inline.
|
|
39
56
|
*/
|
|
40
|
-
create(payload: Payload): TaskRunner;
|
|
57
|
+
create(payload: Payload, handler: TaskHandler): TaskRunner;
|
|
41
58
|
/**
|
|
42
59
|
* Configures the runner and returns a Payload config modifier.
|
|
43
60
|
* The modifier adds necessary tasks, jobs, queues to Payload config.
|
|
61
|
+
* Implementations may also install a `config.onInit` hook for boot-time initialization (e.g. stale-lock recovery).
|
|
44
62
|
*/
|
|
45
63
|
configure(context: TaskRunnerContext): (config: Config) => Config;
|
|
46
64
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export type { TaskRunnerProvider } from
|
|
2
|
-
export type { Task, TaskStatus } from
|
|
3
|
-
export { createPayloadJobsRunner } from
|
|
4
|
-
export type { PayloadJobsRunnerOptions } from
|
|
5
|
-
export { createSyncRunner } from
|
|
6
|
-
export type { TaskRunner } from
|
|
1
|
+
export type { TaskRunnerProvider, TaskRunnerFactory, TaskRunnerContext } from "./TaskRunnerProvider.interface";
|
|
2
|
+
export type { Task, TaskStatus } from "./types";
|
|
3
|
+
export { createPayloadJobsRunner } from "./payload-jobs-runner";
|
|
4
|
+
export type { PayloadJobsRunnerOptions } from "./payload-jobs-runner";
|
|
5
|
+
export { createSyncRunner } from "./sync-runner";
|
|
6
|
+
export type { TaskRunner } from "./TaskRunner.interface";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { createPayloadJobsRunner } from
|
|
2
|
-
export { createSyncRunner } from
|
|
1
|
+
export { createPayloadJobsRunner } from "./payload-jobs-runner";
|
|
2
|
+
export { createSyncRunner } from "./sync-runner";
|
|
3
3
|
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
|
@@ -14,6 +14,21 @@ export declare class PayloadJobsRunnerProvider implements TaskRunnerProvider {
|
|
|
14
14
|
configure(context: TaskRunnerContext): (config: Config) => Config;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Creates
|
|
17
|
+
* Creates the **recommended** task runner: translations run as Payload Jobs
|
|
18
|
+
* (queued, executed by autoRun cron or a manual run, with stale-lock recovery).
|
|
19
|
+
* Durable across restarts and suited to production/serverless. Pass the result
|
|
20
|
+
* as `translatorPlugin({ runner })`.
|
|
21
|
+
*
|
|
22
|
+
* @param options - Queue/task names, `autoRun` cron (or `false` to disable),
|
|
23
|
+
* `staleJobTimeoutMs`, and retry policy. See {@link PayloadJobsRunnerOptions}.
|
|
24
|
+
* @returns A {@link TaskRunnerProvider} for the plugin's `runner` option.
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* translatorPlugin({
|
|
28
|
+
* collections: [Posts],
|
|
29
|
+
* translationProvider: createOpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! }),
|
|
30
|
+
* runner: createPayloadJobsRunner({ autoRun: { cron: '* * * * *' } }),
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
18
33
|
*/
|
|
19
34
|
export declare function createPayloadJobsRunner(options?: PayloadJobsRunnerOptions): TaskRunnerProvider;
|
|
@@ -4,11 +4,13 @@ const defaultAutoRun = {
|
|
|
4
4
|
cron: "* * * * *",
|
|
5
5
|
limit: 50
|
|
6
6
|
};
|
|
7
|
+
const DEFAULT_STALE_JOB_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
7
8
|
const defaultValues = {
|
|
8
9
|
taskName: "translate_document",
|
|
9
10
|
queueName: "translations",
|
|
10
11
|
jobsCollection: "payload-jobs",
|
|
11
12
|
autoRun: defaultAutoRun,
|
|
13
|
+
staleJobTimeoutMs: DEFAULT_STALE_JOB_TIMEOUT_MS,
|
|
12
14
|
retries: {
|
|
13
15
|
attempts: 3,
|
|
14
16
|
backoff: {
|
|
@@ -28,11 +30,16 @@ const defaultValues = {
|
|
|
28
30
|
...defaultAutoRun,
|
|
29
31
|
...options.autoRun
|
|
30
32
|
} : defaultAutoRun;
|
|
33
|
+
const staleJobTimeoutMs = options?.staleJobTimeoutMs ?? defaultValues.staleJobTimeoutMs;
|
|
34
|
+
if (!Number.isFinite(staleJobTimeoutMs) || staleJobTimeoutMs <= 0) {
|
|
35
|
+
throw new Error(`[payload-plugin-translator] staleJobTimeoutMs must be a positive finite number (got ${staleJobTimeoutMs})`);
|
|
36
|
+
}
|
|
31
37
|
this.config = {
|
|
32
38
|
taskName: options?.taskName ?? defaultValues.taskName,
|
|
33
39
|
queueName: options?.queueName ?? defaultValues.queueName,
|
|
34
40
|
jobsCollection: options?.jobsCollection ?? defaultValues.jobsCollection,
|
|
35
41
|
autoRun,
|
|
42
|
+
staleJobTimeoutMs,
|
|
36
43
|
retries: options?.retries ?? defaultValues.retries
|
|
37
44
|
};
|
|
38
45
|
}
|
|
@@ -141,12 +148,46 @@ const defaultValues = {
|
|
|
141
148
|
];
|
|
142
149
|
}
|
|
143
150
|
}
|
|
151
|
+
// Reset stale locks on boot so jobs abandoned by a killed process
|
|
152
|
+
// (deploy/crash/timeout) become eligible for the autorun picker again.
|
|
153
|
+
// The picker requires processing:false, no error, and no pending waitUntil;
|
|
154
|
+
// a mid-run casualty (no error, no waitUntil) satisfies the rest, so
|
|
155
|
+
// clearing processing is sufficient for that case. Threshold-based, so a
|
|
156
|
+
// job genuinely in flight on another live instance (fresh updatedAt) is
|
|
157
|
+
// left alone. Wrapped so a reclaim failure never blocks startup.
|
|
158
|
+
const existingOnInit = config.onInit;
|
|
159
|
+
config.onInit = async (payload)=>{
|
|
160
|
+
if (existingOnInit) await existingOnInit(payload);
|
|
161
|
+
try {
|
|
162
|
+
await new PayloadJobsTaskRunner(payload, this.config).reclaimStaleJobs();
|
|
163
|
+
} catch (err) {
|
|
164
|
+
payload.logger?.error?.({
|
|
165
|
+
err,
|
|
166
|
+
msg: "[translator] failed to reclaim stale translation jobs"
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
144
170
|
return config;
|
|
145
171
|
};
|
|
146
172
|
}
|
|
147
173
|
}
|
|
148
174
|
/**
|
|
149
|
-
* Creates
|
|
175
|
+
* Creates the **recommended** task runner: translations run as Payload Jobs
|
|
176
|
+
* (queued, executed by autoRun cron or a manual run, with stale-lock recovery).
|
|
177
|
+
* Durable across restarts and suited to production/serverless. Pass the result
|
|
178
|
+
* as `translatorPlugin({ runner })`.
|
|
179
|
+
*
|
|
180
|
+
* @param options - Queue/task names, `autoRun` cron (or `false` to disable),
|
|
181
|
+
* `staleJobTimeoutMs`, and retry policy. See {@link PayloadJobsRunnerOptions}.
|
|
182
|
+
* @returns A {@link TaskRunnerProvider} for the plugin's `runner` option.
|
|
183
|
+
* @example
|
|
184
|
+
* ```ts
|
|
185
|
+
* translatorPlugin({
|
|
186
|
+
* collections: [Posts],
|
|
187
|
+
* translationProvider: createOpenAIProvider({ apiKey: process.env.OPENAI_API_KEY! }),
|
|
188
|
+
* runner: createPayloadJobsRunner({ autoRun: { cron: '* * * * *' } }),
|
|
189
|
+
* })
|
|
190
|
+
* ```
|
|
150
191
|
*/ export function createPayloadJobsRunner(options) {
|
|
151
192
|
return new PayloadJobsRunnerProvider(options);
|
|
152
193
|
}
|
|
@@ -14,6 +14,35 @@ export declare class PayloadJobsTaskRunner implements TaskRunner {
|
|
|
14
14
|
enqueue(tasks: TaskInput[]): Promise<void>;
|
|
15
15
|
cancel(taskIds: string[]): Promise<void>;
|
|
16
16
|
run(taskId: string): Promise<RunResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Reset stale processing locks so abandoned jobs become eligible for the
|
|
19
|
+
* autorun picker again. The picker requires processing:false, no error, and
|
|
20
|
+
* no pending waitUntil; a job abandoned mid-run (no error, no waitUntil)
|
|
21
|
+
* satisfies the rest, so clearing processing is sufficient for that case.
|
|
22
|
+
* A job that already exhausted retries (hasError:true) stays excluded from
|
|
23
|
+
* autorun and is only recoverable via a manual run().
|
|
24
|
+
*
|
|
25
|
+
* A job is stale when it is still `processing: true`, not yet completed, and
|
|
26
|
+
* its `updatedAt` is older than `staleJobTimeoutMs` — i.e. a process was
|
|
27
|
+
* killed mid-run (deploy/crash/timeout). Threshold-based, so a job genuinely
|
|
28
|
+
* in flight on another live instance (fresh `updatedAt`) is left alone.
|
|
29
|
+
* Filters on real `payload-jobs` columns only (no JSON-path traversal), so
|
|
30
|
+
* the drizzle SQLite issue in `findByCollection` does not apply here.
|
|
31
|
+
* @returns the number of jobs reclaimed.
|
|
32
|
+
*/
|
|
33
|
+
reclaimStaleJobs(): Promise<number>;
|
|
34
|
+
/**
|
|
35
|
+
* Clear the `processing` lock on every job matching `where`, returning how
|
|
36
|
+
* many were reset. Shared by the per-job reset in `run()` (a stale lock) and
|
|
37
|
+
* the bulk boot/recovery reset in `reclaimStaleJobs()`. `depth: 0` because
|
|
38
|
+
* only the count is needed — no relationships to populate.
|
|
39
|
+
*/
|
|
40
|
+
private resetProcessing;
|
|
41
|
+
/**
|
|
42
|
+
* A processing lock is stale once `updatedAt` is older than the configured
|
|
43
|
+
* timeout — the owning run is presumed dead.
|
|
44
|
+
*/
|
|
45
|
+
private isStale;
|
|
17
46
|
/**
|
|
18
47
|
* Find translation jobs for a collection, optionally narrowed by document IDs.
|
|
19
48
|
*
|
|
@@ -63,19 +63,114 @@ import { normalizeJob } from "./normalizeJob";
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
if (task.status === "running") {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
// A genuinely in-flight job is refused. A stale processing lock (left by
|
|
67
|
+
// a process killed mid-run) is reclaimable: clear it first so the queue
|
|
68
|
+
// picker below — which only selects `processing: false` — can re-run it.
|
|
69
|
+
if (!this.isStale(task.updatedAt)) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
error: "already_running"
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
await this.resetProcessing({
|
|
76
|
+
id: {
|
|
77
|
+
equals: taskId
|
|
78
|
+
}
|
|
79
|
+
});
|
|
70
80
|
}
|
|
71
|
-
|
|
72
|
-
|
|
81
|
+
// Execute synchronously via the queue + `where` picker so the job runs to
|
|
82
|
+
// completion within this request (nothing is abandoned after the HTTP
|
|
83
|
+
// response — reliable on serverless too).
|
|
84
|
+
//
|
|
85
|
+
// NOT `payload.jobs.runByID({ id })`: on the drizzle adapter the id-path
|
|
86
|
+
// (`db.updateJobs({ id })`) writes `processing: true` but returns no rows,
|
|
87
|
+
// so `runJobs` reports `noJobsRemaining` and the handler never runs —
|
|
88
|
+
// leaving the job stuck at `processing: true` forever. The `where`-based
|
|
89
|
+
// picker selects, runs, and finalizes the job correctly (verified against
|
|
90
|
+
// sqlite). The picker also enforces processing:false / no-error / no
|
|
91
|
+
// pending waitUntil, so a failed (max-retries) job is not re-run here.
|
|
92
|
+
await this.payload.jobs.run({
|
|
93
|
+
queue: this.config.queueName,
|
|
94
|
+
where: {
|
|
95
|
+
id: {
|
|
96
|
+
equals: taskId
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
limit: 1
|
|
73
100
|
});
|
|
74
101
|
return {
|
|
75
102
|
success: true
|
|
76
103
|
};
|
|
77
104
|
}
|
|
78
105
|
/**
|
|
106
|
+
* Reset stale processing locks so abandoned jobs become eligible for the
|
|
107
|
+
* autorun picker again. The picker requires processing:false, no error, and
|
|
108
|
+
* no pending waitUntil; a job abandoned mid-run (no error, no waitUntil)
|
|
109
|
+
* satisfies the rest, so clearing processing is sufficient for that case.
|
|
110
|
+
* A job that already exhausted retries (hasError:true) stays excluded from
|
|
111
|
+
* autorun and is only recoverable via a manual run().
|
|
112
|
+
*
|
|
113
|
+
* A job is stale when it is still `processing: true`, not yet completed, and
|
|
114
|
+
* its `updatedAt` is older than `staleJobTimeoutMs` — i.e. a process was
|
|
115
|
+
* killed mid-run (deploy/crash/timeout). Threshold-based, so a job genuinely
|
|
116
|
+
* in flight on another live instance (fresh `updatedAt`) is left alone.
|
|
117
|
+
* Filters on real `payload-jobs` columns only (no JSON-path traversal), so
|
|
118
|
+
* the drizzle SQLite issue in `findByCollection` does not apply here.
|
|
119
|
+
* @returns the number of jobs reclaimed.
|
|
120
|
+
*/ async reclaimStaleJobs() {
|
|
121
|
+
const cutoff = new Date(Date.now() - this.config.staleJobTimeoutMs).toISOString();
|
|
122
|
+
return this.resetProcessing({
|
|
123
|
+
and: [
|
|
124
|
+
{
|
|
125
|
+
taskSlug: {
|
|
126
|
+
equals: this.config.taskName
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
processing: {
|
|
131
|
+
equals: true
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
completedAt: {
|
|
136
|
+
exists: false
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
updatedAt: {
|
|
141
|
+
less_than: cutoff
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
]
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Clear the `processing` lock on every job matching `where`, returning how
|
|
149
|
+
* many were reset. Shared by the per-job reset in `run()` (a stale lock) and
|
|
150
|
+
* the bulk boot/recovery reset in `reclaimStaleJobs()`. `depth: 0` because
|
|
151
|
+
* only the count is needed — no relationships to populate.
|
|
152
|
+
*/ async resetProcessing(where) {
|
|
153
|
+
const result = await this.payload.update({
|
|
154
|
+
collection: this.config.jobsCollection,
|
|
155
|
+
depth: 0,
|
|
156
|
+
where,
|
|
157
|
+
data: {
|
|
158
|
+
processing: false
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return result.docs.length;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A processing lock is stale once `updatedAt` is older than the configured
|
|
165
|
+
* timeout — the owning run is presumed dead.
|
|
166
|
+
*/ isStale(updatedAt) {
|
|
167
|
+
const parsed = Date.parse(updatedAt);
|
|
168
|
+
// Unknown/corrupt timestamp → treat as stale so the job can be recovered
|
|
169
|
+
// rather than permanently refused as already-running.
|
|
170
|
+
if (Number.isNaN(parsed)) return true;
|
|
171
|
+
return Date.now() - parsed > this.config.staleJobTimeoutMs;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
79
174
|
* Find translation jobs for a collection, optionally narrowed by document IDs.
|
|
80
175
|
*
|
|
81
176
|
* Narrowing is by `taskSlug` only in SQL; the collection slug and document
|