@manablox/jobs 0.1.0 → 0.2.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 +18 -0
- package/package.json +3 -3
- package/src/index.ts +17 -105
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# `@manablox/jobs`
|
|
2
|
+
|
|
3
|
+
Background work on BullMQ: webhook delivery with retries, image derivatives, scheduled publishing, key pruning. Without Redis the jobs run inline, which is correct for a single-node install.
|
|
4
|
+
|
|
5
|
+
## Exports
|
|
6
|
+
|
|
7
|
+
- `createJobRunner`, `JobRunner`, `JobPayloads`
|
|
8
|
+
- `attachWebhooks` — turns content hooks into `webhook:deliver` jobs
|
|
9
|
+
|
|
10
|
+
## Depends on
|
|
11
|
+
|
|
12
|
+
@manablox/db, bullmq
|
|
13
|
+
|
|
14
|
+
## Check
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
pnpm --filter @manablox/jobs typecheck
|
|
18
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manablox/jobs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"main": "./src/index.ts",
|
|
12
12
|
"types": "./src/index.ts",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@manablox/core": "0.
|
|
15
|
-
"@manablox/db": "0.
|
|
14
|
+
"@manablox/core": "0.2.0",
|
|
15
|
+
"@manablox/db": "0.2.0",
|
|
16
16
|
"bullmq": "^6.3.4",
|
|
17
17
|
"ioredis": "^6.0.0",
|
|
18
18
|
"drizzle-orm": "^0.45.2"
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { createHmac } from 'node:crypto';
|
|
2
1
|
import type { Manablox } from '@manablox/core';
|
|
3
2
|
import { type Database, type Repositories, schema } from '@manablox/db';
|
|
4
3
|
import { type JobsOptions, Queue, Worker } from 'bullmq';
|
|
5
|
-
import {
|
|
4
|
+
import { sql } from 'drizzle-orm';
|
|
6
5
|
import { Redis } from 'ioredis';
|
|
7
6
|
|
|
8
7
|
export interface JobPayloads {
|
|
9
8
|
'webhook:deliver': { webhookId: string; event: string; payload: Record<string, unknown> };
|
|
10
9
|
'media:derive': { assetId: string; preset: string; format: string };
|
|
11
10
|
'content:scheduledPublish': { contentId: string };
|
|
11
|
+
'workflow:run': { runId: string };
|
|
12
12
|
'maintenance:pruneApiKeys': Record<string, never>;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -21,11 +21,17 @@ export interface JobRunner {
|
|
|
21
21
|
options?: JobsOptions,
|
|
22
22
|
): Promise<void>;
|
|
23
23
|
close(): Promise<void>;
|
|
24
|
+
/** True when there is no queue and `enqueue` runs the job before returning. */
|
|
25
|
+
inline: boolean;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export interface JobHandlers {
|
|
27
29
|
derive?: (assetId: string, preset: string, format: string) => Promise<void>;
|
|
28
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>;
|
|
29
35
|
}
|
|
30
36
|
|
|
31
37
|
/** Background work: webhook delivery with retries, image derivatives, scheduled publish. */
|
|
@@ -41,6 +47,7 @@ export function createJobRunner(
|
|
|
41
47
|
// call sites identical either way.
|
|
42
48
|
if (!redisUrl) {
|
|
43
49
|
return {
|
|
50
|
+
inline: true,
|
|
44
51
|
async enqueue(name, payload) {
|
|
45
52
|
await runJob(name, payload as never, db, repos, handlers, manablox);
|
|
46
53
|
},
|
|
@@ -76,6 +83,7 @@ export function createJobRunner(
|
|
|
76
83
|
});
|
|
77
84
|
|
|
78
85
|
return {
|
|
86
|
+
inline: false,
|
|
79
87
|
async enqueue(name, payload, options) {
|
|
80
88
|
await queue.add(name, payload, {
|
|
81
89
|
attempts: 5,
|
|
@@ -103,7 +111,7 @@ async function runJob(
|
|
|
103
111
|
switch (name) {
|
|
104
112
|
case 'webhook:deliver': {
|
|
105
113
|
const data = payload as JobPayloads['webhook:deliver'];
|
|
106
|
-
await
|
|
114
|
+
await handlers.webhookDeliver?.(data);
|
|
107
115
|
break;
|
|
108
116
|
}
|
|
109
117
|
case 'media:derive': {
|
|
@@ -116,6 +124,11 @@ async function runJob(
|
|
|
116
124
|
await handlers.publish?.(data.contentId);
|
|
117
125
|
break;
|
|
118
126
|
}
|
|
127
|
+
case 'workflow:run': {
|
|
128
|
+
const data = payload as JobPayloads['workflow:run'];
|
|
129
|
+
await handlers.workflowRun?.(data.runId);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
119
132
|
case 'maintenance:pruneApiKeys': {
|
|
120
133
|
await db
|
|
121
134
|
.delete(schema.apikeys)
|
|
@@ -126,106 +139,5 @@ async function runJob(
|
|
|
126
139
|
}
|
|
127
140
|
}
|
|
128
141
|
void repos;
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
async function deliverWebhook(
|
|
132
|
-
db: Database,
|
|
133
|
-
data: JobPayloads['webhook:deliver'],
|
|
134
|
-
manablox: Manablox,
|
|
135
|
-
): Promise<void> {
|
|
136
|
-
const rows = await db
|
|
137
|
-
.select()
|
|
138
|
-
.from(schema.webhooks)
|
|
139
|
-
.where(and(eq(schema.webhooks.id, data.webhookId), eq(schema.webhooks.enabled, true)))
|
|
140
|
-
.limit(1);
|
|
141
|
-
|
|
142
|
-
const webhook = rows[0];
|
|
143
|
-
if (!webhook) return;
|
|
144
|
-
|
|
145
|
-
const body = JSON.stringify({
|
|
146
|
-
event: data.event,
|
|
147
|
-
payload: data.payload,
|
|
148
|
-
at: new Date().toISOString(),
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const headers: Record<string, string> = {
|
|
152
|
-
'content-type': 'application/json',
|
|
153
|
-
'x-manablox-event': data.event,
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
// Signed so a receiver can verify the payload actually came from this instance.
|
|
157
|
-
if (webhook.secret) {
|
|
158
|
-
headers['x-manablox-signature'] = createHmac('sha256', webhook.secret)
|
|
159
|
-
.update(body)
|
|
160
|
-
.digest('hex');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
let status: number | null = null;
|
|
164
|
-
let error: string | null = null;
|
|
165
|
-
|
|
166
|
-
try {
|
|
167
|
-
const response = await fetch(webhook.url, {
|
|
168
|
-
method: 'POST',
|
|
169
|
-
headers,
|
|
170
|
-
body,
|
|
171
|
-
signal: AbortSignal.timeout(10_000),
|
|
172
|
-
});
|
|
173
|
-
status = response.status;
|
|
174
|
-
if (!response.ok) error = `HTTP ${response.status}`;
|
|
175
|
-
} catch (cause) {
|
|
176
|
-
error = cause instanceof Error ? cause.message : String(cause);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
await db.insert(schema.webhookDeliveries).values({
|
|
180
|
-
webhookId: webhook.id,
|
|
181
|
-
event: data.event,
|
|
182
|
-
payload: data.payload,
|
|
183
|
-
status,
|
|
184
|
-
error,
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Throwing hands the retry decision back to BullMQ's backoff policy.
|
|
188
|
-
if (error) {
|
|
189
|
-
manablox.logger.warn({ webhook: webhook.id, status, error }, 'webhook delivery failed');
|
|
190
|
-
throw new Error(error);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** Fans content lifecycle events out to the space's webhooks. */
|
|
195
|
-
export function attachWebhooks(manablox: Manablox, db: Database, jobs: JobRunner): void {
|
|
196
|
-
const dispatch = async (event: string, spaceId: string, payload: Record<string, unknown>) => {
|
|
197
|
-
const hooks = await db
|
|
198
|
-
.select({ id: schema.webhooks.id, events: schema.webhooks.events })
|
|
199
|
-
.from(schema.webhooks)
|
|
200
|
-
.where(and(eq(schema.webhooks.spaceId, spaceId), eq(schema.webhooks.enabled, true)));
|
|
201
|
-
|
|
202
|
-
for (const hook of hooks) {
|
|
203
|
-
if (hook.events.length > 0 && !hook.events.includes(event)) continue;
|
|
204
|
-
await jobs.enqueue('webhook:deliver', { webhookId: hook.id, event, payload });
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
manablox.hooks.on(
|
|
209
|
-
'content:afterPublish',
|
|
210
|
-
async (row) => {
|
|
211
|
-
await dispatch('content.published', row.spaceId, { id: row.id, permalink: row.permalink });
|
|
212
|
-
},
|
|
213
|
-
{ source: '@manablox/jobs' },
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
manablox.hooks.on(
|
|
217
|
-
'content:afterUpdate',
|
|
218
|
-
async (row) => {
|
|
219
|
-
await dispatch('content.updated', row.spaceId, { id: row.id });
|
|
220
|
-
},
|
|
221
|
-
{ source: '@manablox/jobs' },
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
manablox.hooks.on(
|
|
225
|
-
'content:afterDelete',
|
|
226
|
-
async (payload, context) => {
|
|
227
|
-
if (context.spaceId) await dispatch('content.deleted', context.spaceId, { id: payload.id });
|
|
228
|
-
},
|
|
229
|
-
{ source: '@manablox/jobs' },
|
|
230
|
-
);
|
|
142
|
+
void manablox;
|
|
231
143
|
}
|