@manablox/jobs 0.2.0 → 0.4.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/dist/index.d.ts +41 -0
- package/dist/index.js +94 -0
- package/package.json +14 -7
- package/src/index.ts +0 -143
- package/tsconfig.json +0 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Manablox } from "@manablox/core";
|
|
2
|
+
import { Database, Repositories } from "@manablox/db";
|
|
3
|
+
import { JobsOptions } from "bullmq";
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
export interface JobPayloads {
|
|
6
|
+
'webhook:deliver': {
|
|
7
|
+
webhookId: string;
|
|
8
|
+
event: string;
|
|
9
|
+
payload: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
'media:derive': {
|
|
12
|
+
assetId: string;
|
|
13
|
+
preset: string;
|
|
14
|
+
format: string;
|
|
15
|
+
};
|
|
16
|
+
'content:scheduledPublish': {
|
|
17
|
+
contentId: string;
|
|
18
|
+
};
|
|
19
|
+
'workflow:run': {
|
|
20
|
+
runId: string;
|
|
21
|
+
};
|
|
22
|
+
'maintenance:pruneApiKeys': Record<string, never>;
|
|
23
|
+
}
|
|
24
|
+
export type JobName = keyof JobPayloads;
|
|
25
|
+
export interface JobRunner {
|
|
26
|
+
enqueue<K extends JobName>(name: K, payload: JobPayloads[K], options?: JobsOptions): Promise<void>;
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
/** True when there is no queue and `enqueue` runs the job before returning. */
|
|
29
|
+
inline: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface JobHandlers {
|
|
32
|
+
derive?: (assetId: string, preset: string, format: string) => Promise<void>;
|
|
33
|
+
publish?: (contentId: string) => Promise<void>;
|
|
34
|
+
/** Executes a queued workflow run; set by the host once the engine exists. */
|
|
35
|
+
workflowRun?: (runId: string) => Promise<void>;
|
|
36
|
+
/** Sends one webhook delivery; set by the host once the webhook service exists. */
|
|
37
|
+
webhookDeliver?: (data: JobPayloads['webhook:deliver']) => Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
/** Background work: webhook delivery with retries, image derivatives, scheduled publish. */
|
|
40
|
+
export declare function createJobRunner(manablox: Manablox, db: Database, repos: Repositories, handlers?: JobHandlers): JobRunner;
|
|
41
|
+
//#endregion
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { runAsActor, systemActor } from "@manablox/core";
|
|
2
|
+
import { schema } from "@manablox/db";
|
|
3
|
+
import { Queue, Worker } from "bullmq";
|
|
4
|
+
import { sql } from "drizzle-orm";
|
|
5
|
+
import { Redis } from "ioredis";
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
/** Background work: webhook delivery with retries, image derivatives, scheduled publish. */
|
|
8
|
+
function createJobRunner(manablox, db, repos, handlers = {}) {
|
|
9
|
+
const redisUrl = manablox.config.cache.redisUrl;
|
|
10
|
+
if (!redisUrl) return {
|
|
11
|
+
inline: true,
|
|
12
|
+
async enqueue(name, payload) {
|
|
13
|
+
await runAsActor(systemActor(`job:${name}`), () => runJob(name, payload, db, repos, handlers, manablox));
|
|
14
|
+
},
|
|
15
|
+
async close() {}
|
|
16
|
+
};
|
|
17
|
+
const connection = new Redis(redisUrl, { maxRetriesPerRequest: null });
|
|
18
|
+
const queue = new Queue("manablox", { connection });
|
|
19
|
+
const worker = new Worker("manablox", async (job) => {
|
|
20
|
+
await runAsActor(systemActor(`job:${job.name}`), () => runJob(job.name, job.data, db, repos, handlers, manablox));
|
|
21
|
+
}, {
|
|
22
|
+
connection,
|
|
23
|
+
concurrency: 8,
|
|
24
|
+
autorun: true
|
|
25
|
+
});
|
|
26
|
+
worker.on("failed", (job, error) => {
|
|
27
|
+
manablox.logger.warn({
|
|
28
|
+
job: job?.name,
|
|
29
|
+
err: error
|
|
30
|
+
}, "job failed");
|
|
31
|
+
});
|
|
32
|
+
manablox.onDispose(async () => {
|
|
33
|
+
await worker.close();
|
|
34
|
+
await queue.close();
|
|
35
|
+
connection.disconnect();
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
inline: false,
|
|
39
|
+
async enqueue(name, payload, options) {
|
|
40
|
+
await queue.add(name, payload, {
|
|
41
|
+
attempts: 5,
|
|
42
|
+
backoff: {
|
|
43
|
+
type: "exponential",
|
|
44
|
+
delay: 2e3
|
|
45
|
+
},
|
|
46
|
+
removeOnComplete: 500,
|
|
47
|
+
removeOnFail: 1e3,
|
|
48
|
+
...options
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
async close() {
|
|
52
|
+
await worker.close();
|
|
53
|
+
await queue.close();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
async function runJob(name, payload, db, repos, handlers, manablox) {
|
|
58
|
+
switch (name) {
|
|
59
|
+
case "webhook:deliver": {
|
|
60
|
+
const data = payload;
|
|
61
|
+
await handlers.webhookDeliver?.(data);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "media:derive": {
|
|
65
|
+
const data = payload;
|
|
66
|
+
await handlers.derive?.(data.assetId, data.preset, data.format);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case "content:scheduledPublish": {
|
|
70
|
+
const data = payload;
|
|
71
|
+
await handlers.publish?.(data.contentId);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case "workflow:run": {
|
|
75
|
+
const data = payload;
|
|
76
|
+
await handlers.workflowRun?.(data.runId);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case "maintenance:pruneApiKeys": {
|
|
80
|
+
const deleted = await db.delete(schema.apikeys).where(sql`${schema.apikeys.expiresAt} is not null and ${schema.apikeys.expiresAt} < now()`).returning({
|
|
81
|
+
id: schema.apikeys.id,
|
|
82
|
+
name: schema.apikeys.name
|
|
83
|
+
});
|
|
84
|
+
if (deleted.length) await repos.audit.record({
|
|
85
|
+
action: "apiKey.prune",
|
|
86
|
+
targetKind: "apiKey",
|
|
87
|
+
meta: { removed: deleted }
|
|
88
|
+
});
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { createJobRunner };
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manablox/jobs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
|
-
"types": "./
|
|
8
|
-
"default": "./
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"default": "./dist/index.js"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
|
-
"main": "./
|
|
12
|
-
"types": "./
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@manablox/core": "0.
|
|
15
|
-
"@manablox/db": "0.
|
|
14
|
+
"@manablox/core": "0.4.0",
|
|
15
|
+
"@manablox/db": "0.4.0",
|
|
16
16
|
"bullmq": "^6.3.4",
|
|
17
17
|
"ioredis": "^6.0.0",
|
|
18
18
|
"drizzle-orm": "^0.45.2"
|
|
@@ -20,9 +20,16 @@
|
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@manablox/config-typescript": "0.0.0",
|
|
22
22
|
"@types/node": "^26.4.1",
|
|
23
|
+
"tsdown": "^0.23.0",
|
|
23
24
|
"typescript": "^7.0.2"
|
|
24
25
|
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"!dist/**/*.map",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
25
31
|
"scripts": {
|
|
32
|
+
"build": "tsdown",
|
|
26
33
|
"typecheck": "tsc --noEmit"
|
|
27
34
|
}
|
|
28
35
|
}
|
package/src/index.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import type { Manablox } from '@manablox/core';
|
|
2
|
-
import { type Database, type Repositories, schema } from '@manablox/db';
|
|
3
|
-
import { type JobsOptions, Queue, Worker } from 'bullmq';
|
|
4
|
-
import { sql } from 'drizzle-orm';
|
|
5
|
-
import { Redis } from 'ioredis';
|
|
6
|
-
|
|
7
|
-
export interface JobPayloads {
|
|
8
|
-
'webhook:deliver': { webhookId: string; event: string; payload: Record<string, unknown> };
|
|
9
|
-
'media:derive': { assetId: string; preset: string; format: string };
|
|
10
|
-
'content:scheduledPublish': { contentId: string };
|
|
11
|
-
'workflow:run': { runId: string };
|
|
12
|
-
'maintenance:pruneApiKeys': Record<string, never>;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type JobName = keyof JobPayloads;
|
|
16
|
-
|
|
17
|
-
export interface JobRunner {
|
|
18
|
-
enqueue<K extends JobName>(
|
|
19
|
-
name: K,
|
|
20
|
-
payload: JobPayloads[K],
|
|
21
|
-
options?: JobsOptions,
|
|
22
|
-
): Promise<void>;
|
|
23
|
-
close(): Promise<void>;
|
|
24
|
-
/** True when there is no queue and `enqueue` runs the job before returning. */
|
|
25
|
-
inline: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface JobHandlers {
|
|
29
|
-
derive?: (assetId: string, preset: string, format: string) => Promise<void>;
|
|
30
|
-
publish?: (contentId: string) => Promise<void>;
|
|
31
|
-
/** Executes a queued workflow run; set by the host once the engine exists. */
|
|
32
|
-
workflowRun?: (runId: string) => Promise<void>;
|
|
33
|
-
/** Sends one webhook delivery; set by the host once the webhook service exists. */
|
|
34
|
-
webhookDeliver?: (data: JobPayloads['webhook:deliver']) => Promise<void>;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Background work: webhook delivery with retries, image derivatives, scheduled publish. */
|
|
38
|
-
export function createJobRunner(
|
|
39
|
-
manablox: Manablox,
|
|
40
|
-
db: Database,
|
|
41
|
-
repos: Repositories,
|
|
42
|
-
handlers: JobHandlers = {},
|
|
43
|
-
): JobRunner {
|
|
44
|
-
const redisUrl = manablox.config.cache.redisUrl;
|
|
45
|
-
|
|
46
|
-
// Without Redis, jobs run inline. Correct for a single-node install, and it keeps the
|
|
47
|
-
// call sites identical either way.
|
|
48
|
-
if (!redisUrl) {
|
|
49
|
-
return {
|
|
50
|
-
inline: true,
|
|
51
|
-
async enqueue(name, payload) {
|
|
52
|
-
await runJob(name, payload as never, db, repos, handlers, manablox);
|
|
53
|
-
},
|
|
54
|
-
async close() {},
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const connection = new Redis(redisUrl, { maxRetriesPerRequest: null });
|
|
59
|
-
const queue = new Queue('manablox', { connection });
|
|
60
|
-
|
|
61
|
-
const worker = new Worker(
|
|
62
|
-
'manablox',
|
|
63
|
-
async (job) => {
|
|
64
|
-
await runJob(job.name as JobName, job.data, db, repos, handlers, manablox);
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
connection,
|
|
68
|
-
concurrency: 8,
|
|
69
|
-
// Exponential backoff so a webhook endpoint that is briefly down is retried
|
|
70
|
-
// rather than dropped, and a permanently dead one stops being hammered.
|
|
71
|
-
autorun: true,
|
|
72
|
-
},
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
worker.on('failed', (job, error) => {
|
|
76
|
-
manablox.logger.warn({ job: job?.name, err: error }, 'job failed');
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
manablox.onDispose(async () => {
|
|
80
|
-
await worker.close();
|
|
81
|
-
await queue.close();
|
|
82
|
-
connection.disconnect();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
inline: false,
|
|
87
|
-
async enqueue(name, payload, options) {
|
|
88
|
-
await queue.add(name, payload, {
|
|
89
|
-
attempts: 5,
|
|
90
|
-
backoff: { type: 'exponential', delay: 2000 },
|
|
91
|
-
removeOnComplete: 500,
|
|
92
|
-
removeOnFail: 1000,
|
|
93
|
-
...options,
|
|
94
|
-
});
|
|
95
|
-
},
|
|
96
|
-
async close() {
|
|
97
|
-
await worker.close();
|
|
98
|
-
await queue.close();
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async function runJob(
|
|
104
|
-
name: JobName,
|
|
105
|
-
payload: JobPayloads[JobName],
|
|
106
|
-
db: Database,
|
|
107
|
-
repos: Repositories,
|
|
108
|
-
handlers: JobHandlers,
|
|
109
|
-
manablox: Manablox,
|
|
110
|
-
): Promise<void> {
|
|
111
|
-
switch (name) {
|
|
112
|
-
case 'webhook:deliver': {
|
|
113
|
-
const data = payload as JobPayloads['webhook:deliver'];
|
|
114
|
-
await handlers.webhookDeliver?.(data);
|
|
115
|
-
break;
|
|
116
|
-
}
|
|
117
|
-
case 'media:derive': {
|
|
118
|
-
const data = payload as JobPayloads['media:derive'];
|
|
119
|
-
await handlers.derive?.(data.assetId, data.preset, data.format);
|
|
120
|
-
break;
|
|
121
|
-
}
|
|
122
|
-
case 'content:scheduledPublish': {
|
|
123
|
-
const data = payload as JobPayloads['content:scheduledPublish'];
|
|
124
|
-
await handlers.publish?.(data.contentId);
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
case 'workflow:run': {
|
|
128
|
-
const data = payload as JobPayloads['workflow:run'];
|
|
129
|
-
await handlers.workflowRun?.(data.runId);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
case 'maintenance:pruneApiKeys': {
|
|
133
|
-
await db
|
|
134
|
-
.delete(schema.apikeys)
|
|
135
|
-
.where(
|
|
136
|
-
sql`${schema.apikeys.expiresAt} is not null and ${schema.apikeys.expiresAt} < now()`,
|
|
137
|
-
);
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
void repos;
|
|
142
|
-
void manablox;
|
|
143
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{ "extends": "@manablox/config-typescript/library.json", "include": ["src"] }
|