@sourcepress/jobs 0.1.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/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +14 -0
- package/dist/__tests__/in-process.test.d.ts +2 -0
- package/dist/__tests__/in-process.test.d.ts.map +1 -0
- package/dist/__tests__/in-process.test.js +125 -0
- package/dist/__tests__/in-process.test.js.map +1 -0
- package/dist/in-process.d.ts +18 -0
- package/dist/in-process.d.ts.map +1 -0
- package/dist/in-process.js +106 -0
- package/dist/in-process.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +35 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +25 -0
- package/src/__tests__/in-process.test.ts +162 -0
- package/src/in-process.ts +132 -0
- package/src/index.ts +2 -0
- package/src/types.ts +43 -0
- package/tsconfig.json +5 -0
- package/vitest.config.ts +2 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
|
|
2
|
+
> @sourcepress/jobs@0.1.0 test /Users/fabianvontiedemann/Developer/org/sourcepress/packages/jobs
|
|
3
|
+
> vitest run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
RUN v3.2.4 /Users/fabianvontiedemann/Developer/org/sourcepress/packages/jobs
|
|
7
|
+
|
|
8
|
+
✓ src/__tests__/in-process.test.ts (10 tests) 346ms
|
|
9
|
+
|
|
10
|
+
Test Files 1 passed (1)
|
|
11
|
+
Tests 10 passed (10)
|
|
12
|
+
Start at 00:37:10
|
|
13
|
+
Duration 1.44s (transform 33ms, setup 0ms, collect 37ms, tests 346ms, environment 0ms, prepare 330ms)
|
|
14
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-process.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/in-process.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { InProcessJobProvider } from "../in-process.js";
|
|
3
|
+
describe("InProcessJobProvider", () => {
|
|
4
|
+
it("registers a handler and enqueues a job", async () => {
|
|
5
|
+
const provider = new InProcessJobProvider();
|
|
6
|
+
provider.register("test-job", async (params, progress) => {
|
|
7
|
+
progress(0, 1);
|
|
8
|
+
progress(1, 1);
|
|
9
|
+
return { success: true };
|
|
10
|
+
});
|
|
11
|
+
const jobId = await provider.enqueue({
|
|
12
|
+
type: "test-job",
|
|
13
|
+
params: { input: "hello" },
|
|
14
|
+
});
|
|
15
|
+
expect(jobId).toMatch(/^job_/);
|
|
16
|
+
});
|
|
17
|
+
it("throws when enqueuing unregistered job type", async () => {
|
|
18
|
+
const provider = new InProcessJobProvider();
|
|
19
|
+
await expect(provider.enqueue({ type: "unknown", params: {} })).rejects.toThrow("No handler registered");
|
|
20
|
+
});
|
|
21
|
+
it("runs job to completion and stores result", async () => {
|
|
22
|
+
const provider = new InProcessJobProvider();
|
|
23
|
+
provider.register("fast-job", async () => {
|
|
24
|
+
return { processed: 42 };
|
|
25
|
+
});
|
|
26
|
+
const jobId = await provider.enqueue({
|
|
27
|
+
type: "fast-job",
|
|
28
|
+
params: {},
|
|
29
|
+
});
|
|
30
|
+
// Wait for async execution
|
|
31
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
32
|
+
const status = await provider.status(jobId);
|
|
33
|
+
expect(status).not.toBeNull();
|
|
34
|
+
expect(status?.status).toBe("completed");
|
|
35
|
+
expect(status?.result).toEqual({ processed: 42 });
|
|
36
|
+
expect(status?.started_at).toBeTruthy();
|
|
37
|
+
expect(status?.completed_at).toBeTruthy();
|
|
38
|
+
});
|
|
39
|
+
it("tracks progress during execution", async () => {
|
|
40
|
+
const provider = new InProcessJobProvider();
|
|
41
|
+
provider.register("progress-job", async (_params, progress) => {
|
|
42
|
+
progress(0, 10);
|
|
43
|
+
progress(5, 10);
|
|
44
|
+
progress(10, 10);
|
|
45
|
+
return "done";
|
|
46
|
+
});
|
|
47
|
+
const jobId = await provider.enqueue({
|
|
48
|
+
type: "progress-job",
|
|
49
|
+
params: {},
|
|
50
|
+
});
|
|
51
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
52
|
+
const status = await provider.status(jobId);
|
|
53
|
+
expect(status?.progress.completed).toBe(10);
|
|
54
|
+
expect(status?.progress.total).toBe(10);
|
|
55
|
+
});
|
|
56
|
+
it("handles job failure gracefully", async () => {
|
|
57
|
+
const provider = new InProcessJobProvider();
|
|
58
|
+
provider.register("failing-job", async () => {
|
|
59
|
+
throw new Error("Something went wrong");
|
|
60
|
+
});
|
|
61
|
+
const jobId = await provider.enqueue({
|
|
62
|
+
type: "failing-job",
|
|
63
|
+
params: {},
|
|
64
|
+
});
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
66
|
+
const status = await provider.status(jobId);
|
|
67
|
+
expect(status?.status).toBe("failed");
|
|
68
|
+
expect(status?.error).toBe("Something went wrong");
|
|
69
|
+
});
|
|
70
|
+
it("cancels a running job", async () => {
|
|
71
|
+
const provider = new InProcessJobProvider();
|
|
72
|
+
provider.register("slow-job", async (_params, progress) => {
|
|
73
|
+
progress(0, 100);
|
|
74
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
75
|
+
return "should not reach";
|
|
76
|
+
});
|
|
77
|
+
const jobId = await provider.enqueue({
|
|
78
|
+
type: "slow-job",
|
|
79
|
+
params: {},
|
|
80
|
+
});
|
|
81
|
+
// Let it start
|
|
82
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
83
|
+
await provider.cancel(jobId);
|
|
84
|
+
const status = await provider.status(jobId);
|
|
85
|
+
expect(status?.status).toBe("cancelled");
|
|
86
|
+
});
|
|
87
|
+
it("lists jobs with status filter", async () => {
|
|
88
|
+
const provider = new InProcessJobProvider();
|
|
89
|
+
provider.register("quick-job", async () => "ok");
|
|
90
|
+
await provider.enqueue({ type: "quick-job", params: {} });
|
|
91
|
+
await provider.enqueue({ type: "quick-job", params: {} });
|
|
92
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
93
|
+
const completed = await provider.list({ status: "completed" });
|
|
94
|
+
expect(completed.length).toBe(2);
|
|
95
|
+
const running = await provider.list({ status: "running" });
|
|
96
|
+
expect(running.length).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
it("lists jobs with type filter", async () => {
|
|
99
|
+
const provider = new InProcessJobProvider();
|
|
100
|
+
provider.register("type-a", async () => "a");
|
|
101
|
+
provider.register("type-b", async () => "b");
|
|
102
|
+
await provider.enqueue({ type: "type-a", params: {} });
|
|
103
|
+
await provider.enqueue({ type: "type-b", params: {} });
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
105
|
+
const typeA = await provider.list({ type: "type-a" });
|
|
106
|
+
expect(typeA.length).toBe(1);
|
|
107
|
+
expect(typeA[0].type).toBe("type-a");
|
|
108
|
+
});
|
|
109
|
+
it("lists jobs with limit", async () => {
|
|
110
|
+
const provider = new InProcessJobProvider();
|
|
111
|
+
provider.register("many-job", async () => "ok");
|
|
112
|
+
await provider.enqueue({ type: "many-job", params: {} });
|
|
113
|
+
await provider.enqueue({ type: "many-job", params: {} });
|
|
114
|
+
await provider.enqueue({ type: "many-job", params: {} });
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
116
|
+
const limited = await provider.list({ limit: 2 });
|
|
117
|
+
expect(limited.length).toBe(2);
|
|
118
|
+
});
|
|
119
|
+
it("returns null for unknown job id", async () => {
|
|
120
|
+
const provider = new InProcessJobProvider();
|
|
121
|
+
const status = await provider.status("nonexistent");
|
|
122
|
+
expect(status).toBeNull();
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
//# sourceMappingURL=in-process.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-process.test.js","sourceRoot":"","sources":["../../src/__tests__/in-process.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { JobDefinition, JobFilter, JobStatus } from "@sourcepress/core";
|
|
2
|
+
import type { JobHandler, JobProvider } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* In-process job provider. Runs jobs as async tasks in the current process.
|
|
5
|
+
* Best for development and small instances. No persistence — jobs are lost on restart.
|
|
6
|
+
*/
|
|
7
|
+
export declare class InProcessJobProvider implements JobProvider {
|
|
8
|
+
private handlers;
|
|
9
|
+
private jobs;
|
|
10
|
+
private abortControllers;
|
|
11
|
+
register(type: string, handler: JobHandler): void;
|
|
12
|
+
enqueue(job: JobDefinition): Promise<string>;
|
|
13
|
+
status(jobId: string): Promise<JobStatus | null>;
|
|
14
|
+
cancel(jobId: string): Promise<void>;
|
|
15
|
+
list(filter?: JobFilter): Promise<JobStatus[]>;
|
|
16
|
+
private executeJob;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=in-process.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-process.d.ts","sourceRoot":"","sources":["../src/in-process.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC7E,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAS1D;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,WAAW;IACvD,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,IAAI,CAAgC;IAC5C,OAAO,CAAC,gBAAgB,CAAsC;IAE9D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI;IAI3C,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IA6B5C,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC;IAIhD,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgBpC,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;YAqBtC,UAAU;CAqCxB"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Generate a cryptographically secure unique ID for jobs.
|
|
4
|
+
*/
|
|
5
|
+
function generateJobId() {
|
|
6
|
+
return `job_${Date.now()}_${randomBytes(12).toString("hex")}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* In-process job provider. Runs jobs as async tasks in the current process.
|
|
10
|
+
* Best for development and small instances. No persistence — jobs are lost on restart.
|
|
11
|
+
*/
|
|
12
|
+
export class InProcessJobProvider {
|
|
13
|
+
handlers = new Map();
|
|
14
|
+
jobs = new Map();
|
|
15
|
+
abortControllers = new Map();
|
|
16
|
+
register(type, handler) {
|
|
17
|
+
this.handlers.set(type, handler);
|
|
18
|
+
}
|
|
19
|
+
async enqueue(job) {
|
|
20
|
+
const handler = this.handlers.get(job.type);
|
|
21
|
+
if (!handler) {
|
|
22
|
+
throw new Error(`No handler registered for job type: ${job.type}`);
|
|
23
|
+
}
|
|
24
|
+
const jobId = generateJobId();
|
|
25
|
+
const now = new Date().toISOString();
|
|
26
|
+
const status = {
|
|
27
|
+
job_id: jobId,
|
|
28
|
+
type: job.type,
|
|
29
|
+
status: "queued",
|
|
30
|
+
progress: { completed: 0, total: 0, failed: 0 },
|
|
31
|
+
created_at: now,
|
|
32
|
+
};
|
|
33
|
+
this.jobs.set(jobId, status);
|
|
34
|
+
// Create abort controller for cancellation
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
this.abortControllers.set(jobId, controller);
|
|
37
|
+
// Run async — do not await
|
|
38
|
+
this.executeJob(jobId, job, handler, controller.signal);
|
|
39
|
+
return jobId;
|
|
40
|
+
}
|
|
41
|
+
async status(jobId) {
|
|
42
|
+
return this.jobs.get(jobId) ?? null;
|
|
43
|
+
}
|
|
44
|
+
async cancel(jobId) {
|
|
45
|
+
const job = this.jobs.get(jobId);
|
|
46
|
+
if (!job)
|
|
47
|
+
return;
|
|
48
|
+
if (job.status === "queued" || job.status === "running") {
|
|
49
|
+
job.status = "cancelled";
|
|
50
|
+
job.completed_at = new Date().toISOString();
|
|
51
|
+
const controller = this.abortControllers.get(jobId);
|
|
52
|
+
if (controller) {
|
|
53
|
+
controller.abort();
|
|
54
|
+
this.abortControllers.delete(jobId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async list(filter) {
|
|
59
|
+
let jobs = Array.from(this.jobs.values());
|
|
60
|
+
if (filter?.status) {
|
|
61
|
+
jobs = jobs.filter((j) => j.status === filter.status);
|
|
62
|
+
}
|
|
63
|
+
if (filter?.type) {
|
|
64
|
+
jobs = jobs.filter((j) => j.type === filter.type);
|
|
65
|
+
}
|
|
66
|
+
// Sort by created_at descending (most recent first)
|
|
67
|
+
jobs.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
68
|
+
if (filter?.limit) {
|
|
69
|
+
jobs = jobs.slice(0, filter.limit);
|
|
70
|
+
}
|
|
71
|
+
return jobs;
|
|
72
|
+
}
|
|
73
|
+
async executeJob(jobId, job, handler, signal) {
|
|
74
|
+
const status = this.jobs.get(jobId);
|
|
75
|
+
if (!status)
|
|
76
|
+
return;
|
|
77
|
+
// Transition to running
|
|
78
|
+
status.status = "running";
|
|
79
|
+
status.started_at = new Date().toISOString();
|
|
80
|
+
try {
|
|
81
|
+
// Progress callback
|
|
82
|
+
const progress = (completed, total, failed = 0) => {
|
|
83
|
+
if (signal.aborted)
|
|
84
|
+
return;
|
|
85
|
+
status.progress = { completed, total, failed };
|
|
86
|
+
};
|
|
87
|
+
const result = await handler(job.params, progress);
|
|
88
|
+
if (signal.aborted)
|
|
89
|
+
return;
|
|
90
|
+
status.status = "completed";
|
|
91
|
+
status.result = result;
|
|
92
|
+
status.completed_at = new Date().toISOString();
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (signal.aborted)
|
|
96
|
+
return;
|
|
97
|
+
status.status = "failed";
|
|
98
|
+
status.error = error instanceof Error ? error.message : String(error);
|
|
99
|
+
status.completed_at = new Date().toISOString();
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
this.abortControllers.delete(jobId);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=in-process.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"in-process.js","sourceRoot":"","sources":["../src/in-process.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { JobDefinition, JobFilter, JobStatus } from "@sourcepress/core";
|
|
2
|
+
/**
|
|
3
|
+
* Handler function for a job type.
|
|
4
|
+
* Receives the job params and a progress callback.
|
|
5
|
+
* Returns the result (stored in job.result).
|
|
6
|
+
*/
|
|
7
|
+
export type JobHandler = (params: Record<string, unknown>, progress: (completed: number, total: number, failed?: number) => void) => Promise<unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* Pluggable job provider interface.
|
|
10
|
+
* Implementations: InProcessJobProvider (default), CloudflareQueuesProvider, BullMQProvider (future).
|
|
11
|
+
*/
|
|
12
|
+
export interface JobProvider {
|
|
13
|
+
/**
|
|
14
|
+
* Register a handler for a job type.
|
|
15
|
+
* Must be called before enqueuing jobs of that type.
|
|
16
|
+
*/
|
|
17
|
+
register(type: string, handler: JobHandler): void;
|
|
18
|
+
/**
|
|
19
|
+
* Enqueue a job for async execution. Returns the job_id.
|
|
20
|
+
*/
|
|
21
|
+
enqueue(job: JobDefinition): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Get the current status of a job.
|
|
24
|
+
*/
|
|
25
|
+
status(jobId: string): Promise<JobStatus | null>;
|
|
26
|
+
/**
|
|
27
|
+
* Cancel a running or queued job.
|
|
28
|
+
*/
|
|
29
|
+
cancel(jobId: string): Promise<void>;
|
|
30
|
+
/**
|
|
31
|
+
* List jobs, optionally filtered by status/type.
|
|
32
|
+
*/
|
|
33
|
+
list(filter?: JobFilter): Promise<JobStatus[]>;
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE7E;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,CACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,QAAQ,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,KACjE,OAAO,CAAC,OAAO,CAAC,CAAC;AAEtB;;;GAGG;AACH,MAAM,WAAW,WAAW;IAC3B;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,IAAI,CAAC;IAElD;;OAEG;IACH,OAAO,CAAC,GAAG,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAE7C;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IAEjD;;OAEG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAErC;;OAEG;IACH,IAAI,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC,CAAC;CAC/C"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sourcepress/jobs",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": { "access": "public" },
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"clean": "rm -rf dist"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@sourcepress/core": "workspace:*"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "^5.7.0",
|
|
23
|
+
"vitest": "^3.0.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { InProcessJobProvider } from "../in-process.js";
|
|
3
|
+
|
|
4
|
+
describe("InProcessJobProvider", () => {
|
|
5
|
+
it("registers a handler and enqueues a job", async () => {
|
|
6
|
+
const provider = new InProcessJobProvider();
|
|
7
|
+
provider.register("test-job", async (params, progress) => {
|
|
8
|
+
progress(0, 1);
|
|
9
|
+
progress(1, 1);
|
|
10
|
+
return { success: true };
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const jobId = await provider.enqueue({
|
|
14
|
+
type: "test-job",
|
|
15
|
+
params: { input: "hello" },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
expect(jobId).toMatch(/^job_/);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("throws when enqueuing unregistered job type", async () => {
|
|
22
|
+
const provider = new InProcessJobProvider();
|
|
23
|
+
|
|
24
|
+
await expect(provider.enqueue({ type: "unknown", params: {} })).rejects.toThrow(
|
|
25
|
+
"No handler registered",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("runs job to completion and stores result", async () => {
|
|
30
|
+
const provider = new InProcessJobProvider();
|
|
31
|
+
provider.register("fast-job", async () => {
|
|
32
|
+
return { processed: 42 };
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const jobId = await provider.enqueue({
|
|
36
|
+
type: "fast-job",
|
|
37
|
+
params: {},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Wait for async execution
|
|
41
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
42
|
+
|
|
43
|
+
const status = await provider.status(jobId);
|
|
44
|
+
expect(status).not.toBeNull();
|
|
45
|
+
expect(status?.status).toBe("completed");
|
|
46
|
+
expect(status?.result).toEqual({ processed: 42 });
|
|
47
|
+
expect(status?.started_at).toBeTruthy();
|
|
48
|
+
expect(status?.completed_at).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("tracks progress during execution", async () => {
|
|
52
|
+
const provider = new InProcessJobProvider();
|
|
53
|
+
provider.register("progress-job", async (_params, progress) => {
|
|
54
|
+
progress(0, 10);
|
|
55
|
+
progress(5, 10);
|
|
56
|
+
progress(10, 10);
|
|
57
|
+
return "done";
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const jobId = await provider.enqueue({
|
|
61
|
+
type: "progress-job",
|
|
62
|
+
params: {},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
66
|
+
|
|
67
|
+
const status = await provider.status(jobId);
|
|
68
|
+
expect(status?.progress.completed).toBe(10);
|
|
69
|
+
expect(status?.progress.total).toBe(10);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("handles job failure gracefully", async () => {
|
|
73
|
+
const provider = new InProcessJobProvider();
|
|
74
|
+
provider.register("failing-job", async () => {
|
|
75
|
+
throw new Error("Something went wrong");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const jobId = await provider.enqueue({
|
|
79
|
+
type: "failing-job",
|
|
80
|
+
params: {},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
84
|
+
|
|
85
|
+
const status = await provider.status(jobId);
|
|
86
|
+
expect(status?.status).toBe("failed");
|
|
87
|
+
expect(status?.error).toBe("Something went wrong");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("cancels a running job", async () => {
|
|
91
|
+
const provider = new InProcessJobProvider();
|
|
92
|
+
provider.register("slow-job", async (_params, progress) => {
|
|
93
|
+
progress(0, 100);
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
95
|
+
return "should not reach";
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const jobId = await provider.enqueue({
|
|
99
|
+
type: "slow-job",
|
|
100
|
+
params: {},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Let it start
|
|
104
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
105
|
+
|
|
106
|
+
await provider.cancel(jobId);
|
|
107
|
+
|
|
108
|
+
const status = await provider.status(jobId);
|
|
109
|
+
expect(status?.status).toBe("cancelled");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("lists jobs with status filter", async () => {
|
|
113
|
+
const provider = new InProcessJobProvider();
|
|
114
|
+
provider.register("quick-job", async () => "ok");
|
|
115
|
+
|
|
116
|
+
await provider.enqueue({ type: "quick-job", params: {} });
|
|
117
|
+
await provider.enqueue({ type: "quick-job", params: {} });
|
|
118
|
+
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
120
|
+
|
|
121
|
+
const completed = await provider.list({ status: "completed" });
|
|
122
|
+
expect(completed.length).toBe(2);
|
|
123
|
+
|
|
124
|
+
const running = await provider.list({ status: "running" });
|
|
125
|
+
expect(running.length).toBe(0);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("lists jobs with type filter", async () => {
|
|
129
|
+
const provider = new InProcessJobProvider();
|
|
130
|
+
provider.register("type-a", async () => "a");
|
|
131
|
+
provider.register("type-b", async () => "b");
|
|
132
|
+
|
|
133
|
+
await provider.enqueue({ type: "type-a", params: {} });
|
|
134
|
+
await provider.enqueue({ type: "type-b", params: {} });
|
|
135
|
+
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
137
|
+
|
|
138
|
+
const typeA = await provider.list({ type: "type-a" });
|
|
139
|
+
expect(typeA.length).toBe(1);
|
|
140
|
+
expect(typeA[0].type).toBe("type-a");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("lists jobs with limit", async () => {
|
|
144
|
+
const provider = new InProcessJobProvider();
|
|
145
|
+
provider.register("many-job", async () => "ok");
|
|
146
|
+
|
|
147
|
+
await provider.enqueue({ type: "many-job", params: {} });
|
|
148
|
+
await provider.enqueue({ type: "many-job", params: {} });
|
|
149
|
+
await provider.enqueue({ type: "many-job", params: {} });
|
|
150
|
+
|
|
151
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
152
|
+
|
|
153
|
+
const limited = await provider.list({ limit: 2 });
|
|
154
|
+
expect(limited.length).toBe(2);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("returns null for unknown job id", async () => {
|
|
158
|
+
const provider = new InProcessJobProvider();
|
|
159
|
+
const status = await provider.status("nonexistent");
|
|
160
|
+
expect(status).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import type { JobDefinition, JobFilter, JobStatus } from "@sourcepress/core";
|
|
3
|
+
import type { JobHandler, JobProvider } from "./types.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generate a cryptographically secure unique ID for jobs.
|
|
7
|
+
*/
|
|
8
|
+
function generateJobId(): string {
|
|
9
|
+
return `job_${Date.now()}_${randomBytes(12).toString("hex")}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* In-process job provider. Runs jobs as async tasks in the current process.
|
|
14
|
+
* Best for development and small instances. No persistence — jobs are lost on restart.
|
|
15
|
+
*/
|
|
16
|
+
export class InProcessJobProvider implements JobProvider {
|
|
17
|
+
private handlers = new Map<string, JobHandler>();
|
|
18
|
+
private jobs = new Map<string, JobStatus>();
|
|
19
|
+
private abortControllers = new Map<string, AbortController>();
|
|
20
|
+
|
|
21
|
+
register(type: string, handler: JobHandler): void {
|
|
22
|
+
this.handlers.set(type, handler);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async enqueue(job: JobDefinition): Promise<string> {
|
|
26
|
+
const handler = this.handlers.get(job.type);
|
|
27
|
+
if (!handler) {
|
|
28
|
+
throw new Error(`No handler registered for job type: ${job.type}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const jobId = generateJobId();
|
|
32
|
+
const now = new Date().toISOString();
|
|
33
|
+
|
|
34
|
+
const status: JobStatus = {
|
|
35
|
+
job_id: jobId,
|
|
36
|
+
type: job.type,
|
|
37
|
+
status: "queued",
|
|
38
|
+
progress: { completed: 0, total: 0, failed: 0 },
|
|
39
|
+
created_at: now,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
this.jobs.set(jobId, status);
|
|
43
|
+
|
|
44
|
+
// Create abort controller for cancellation
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
this.abortControllers.set(jobId, controller);
|
|
47
|
+
|
|
48
|
+
// Run async — do not await
|
|
49
|
+
this.executeJob(jobId, job, handler, controller.signal);
|
|
50
|
+
|
|
51
|
+
return jobId;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async status(jobId: string): Promise<JobStatus | null> {
|
|
55
|
+
return this.jobs.get(jobId) ?? null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async cancel(jobId: string): Promise<void> {
|
|
59
|
+
const job = this.jobs.get(jobId);
|
|
60
|
+
if (!job) return;
|
|
61
|
+
|
|
62
|
+
if (job.status === "queued" || job.status === "running") {
|
|
63
|
+
job.status = "cancelled";
|
|
64
|
+
job.completed_at = new Date().toISOString();
|
|
65
|
+
|
|
66
|
+
const controller = this.abortControllers.get(jobId);
|
|
67
|
+
if (controller) {
|
|
68
|
+
controller.abort();
|
|
69
|
+
this.abortControllers.delete(jobId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async list(filter?: JobFilter): Promise<JobStatus[]> {
|
|
75
|
+
let jobs = Array.from(this.jobs.values());
|
|
76
|
+
|
|
77
|
+
if (filter?.status) {
|
|
78
|
+
jobs = jobs.filter((j) => j.status === filter.status);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (filter?.type) {
|
|
82
|
+
jobs = jobs.filter((j) => j.type === filter.type);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Sort by created_at descending (most recent first)
|
|
86
|
+
jobs.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
87
|
+
|
|
88
|
+
if (filter?.limit) {
|
|
89
|
+
jobs = jobs.slice(0, filter.limit);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return jobs;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async executeJob(
|
|
96
|
+
jobId: string,
|
|
97
|
+
job: JobDefinition,
|
|
98
|
+
handler: JobHandler,
|
|
99
|
+
signal: AbortSignal,
|
|
100
|
+
): Promise<void> {
|
|
101
|
+
const status = this.jobs.get(jobId);
|
|
102
|
+
if (!status) return;
|
|
103
|
+
|
|
104
|
+
// Transition to running
|
|
105
|
+
status.status = "running";
|
|
106
|
+
status.started_at = new Date().toISOString();
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Progress callback
|
|
110
|
+
const progress = (completed: number, total: number, failed = 0) => {
|
|
111
|
+
if (signal.aborted) return;
|
|
112
|
+
status.progress = { completed, total, failed };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const result = await handler(job.params, progress);
|
|
116
|
+
|
|
117
|
+
if (signal.aborted) return;
|
|
118
|
+
|
|
119
|
+
status.status = "completed";
|
|
120
|
+
status.result = result;
|
|
121
|
+
status.completed_at = new Date().toISOString();
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (signal.aborted) return;
|
|
124
|
+
|
|
125
|
+
status.status = "failed";
|
|
126
|
+
status.error = error instanceof Error ? error.message : String(error);
|
|
127
|
+
status.completed_at = new Date().toISOString();
|
|
128
|
+
} finally {
|
|
129
|
+
this.abortControllers.delete(jobId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { JobDefinition, JobFilter, JobStatus } from "@sourcepress/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handler function for a job type.
|
|
5
|
+
* Receives the job params and a progress callback.
|
|
6
|
+
* Returns the result (stored in job.result).
|
|
7
|
+
*/
|
|
8
|
+
export type JobHandler = (
|
|
9
|
+
params: Record<string, unknown>,
|
|
10
|
+
progress: (completed: number, total: number, failed?: number) => void,
|
|
11
|
+
) => Promise<unknown>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Pluggable job provider interface.
|
|
15
|
+
* Implementations: InProcessJobProvider (default), CloudflareQueuesProvider, BullMQProvider (future).
|
|
16
|
+
*/
|
|
17
|
+
export interface JobProvider {
|
|
18
|
+
/**
|
|
19
|
+
* Register a handler for a job type.
|
|
20
|
+
* Must be called before enqueuing jobs of that type.
|
|
21
|
+
*/
|
|
22
|
+
register(type: string, handler: JobHandler): void;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Enqueue a job for async execution. Returns the job_id.
|
|
26
|
+
*/
|
|
27
|
+
enqueue(job: JobDefinition): Promise<string>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the current status of a job.
|
|
31
|
+
*/
|
|
32
|
+
status(jobId: string): Promise<JobStatus | null>;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Cancel a running or queued job.
|
|
36
|
+
*/
|
|
37
|
+
cancel(jobId: string): Promise<void>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* List jobs, optionally filtered by status/type.
|
|
41
|
+
*/
|
|
42
|
+
list(filter?: JobFilter): Promise<JobStatus[]>;
|
|
43
|
+
}
|
package/tsconfig.json
ADDED
package/vitest.config.ts
ADDED