@yellowpanther/shared 1.2.5 → 1.2.7
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/package.json +3 -2
- package/src/queue/articlePublishQueue.js +50 -23
- package/src/queue/imageAlbumPublishQueue.js +49 -23
- package/src/queue/imagePublishQueue.js +46 -23
- package/src/queue/newsPublishQueue.js +46 -23
- package/src/queue/pagePublishQueue.js +46 -23
- package/src/queue/predictionPublishQueue.js +50 -23
- package/src/queue/productPublishQueue.js +50 -23
- package/src/queue/quizPublishQueue.js +46 -23
- package/src/queue/videoPublishQueue.js +46 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yellowpanther/shared",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"license": "MIT",
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"dotenv": "^17.2.2"
|
|
43
|
+
"dotenv": "^17.2.2",
|
|
44
|
+
"joi": "^18.0.1"
|
|
44
45
|
}
|
|
45
46
|
}
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
// One-off *publish* scheduler for article items (BullMQ).
|
|
3
3
|
// Strong upsert: purge any prior job for the same article before adding a new delayed job.
|
|
4
4
|
|
|
5
|
-
const { Queue } = require(
|
|
6
|
-
const redisClient = require(
|
|
5
|
+
const { Queue } = require("bullmq");
|
|
6
|
+
const redisClient = require("../redis/redisClient");
|
|
7
7
|
// const logger = require('../logger'); // uncomment if you have a shared logger
|
|
8
8
|
|
|
9
|
-
const DEBUG = String(process.env.DEBUG_LOGGER ||
|
|
9
|
+
const DEBUG = String(process.env.DEBUG_LOGGER || "").trim() === "1";
|
|
10
10
|
const DRIFT_MS = Math.max(0, Number(process.env.SCHEDULE_DRIFT_MS || 250));
|
|
11
11
|
|
|
12
|
-
const articlePublishQueue = new Queue(
|
|
12
|
+
const articlePublishQueue = new Queue("articlePublishQueue", {
|
|
13
13
|
connection: redisClient,
|
|
14
14
|
defaultJobOptions: {
|
|
15
15
|
attempts: 5,
|
|
16
|
-
backoff: { type:
|
|
16
|
+
backoff: { type: "exponential", delay: 2000 },
|
|
17
17
|
removeOnComplete: 500,
|
|
18
18
|
removeOnFail: 500,
|
|
19
19
|
},
|
|
@@ -21,11 +21,19 @@ const articlePublishQueue = new Queue('articlePublishQueue', {
|
|
|
21
21
|
|
|
22
22
|
// --- ID helpers --------------------------------------------------------------
|
|
23
23
|
|
|
24
|
+
/** Replace invalid chars for BullMQ IDs */
|
|
25
|
+
function sanitizeId(str) {
|
|
26
|
+
return str.replace(/[:.]/g, "-");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Stable id (latest schedule) */
|
|
24
30
|
function stableJobId(article_id) {
|
|
25
|
-
return `article:publish:${article_id}
|
|
31
|
+
return sanitizeId(`article:publish:${article_id}`);
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Versioned id (keeps history distinct) */
|
|
27
35
|
function versionedJobId(article_id, runAtIso) {
|
|
28
|
-
return `article:publish:${article_id}:${runAtIso}
|
|
36
|
+
return sanitizeId(`article:publish:${article_id}:${runAtIso}`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
// --- Time helpers ------------------------------------------------------------
|
|
@@ -33,19 +41,21 @@ function versionedJobId(article_id, runAtIso) {
|
|
|
33
41
|
/** Parse anything into a UTC Date. Strings without timezone are treated as UTC. */
|
|
34
42
|
function normalizeToUtcDate(input) {
|
|
35
43
|
if (input instanceof Date) return new Date(input.getTime());
|
|
36
|
-
if (typeof input ===
|
|
44
|
+
if (typeof input === "number") return new Date(input); // epoch ms
|
|
37
45
|
|
|
38
|
-
if (typeof input ===
|
|
46
|
+
if (typeof input === "string") {
|
|
39
47
|
// Has timezone (Z or ±hh:mm)
|
|
40
48
|
if (/[zZ]|[+\-]\d{2}:\d{2}$/.test(input)) {
|
|
41
49
|
const d = new Date(input);
|
|
42
|
-
if (Number.isNaN(d.getTime()))
|
|
50
|
+
if (Number.isNaN(d.getTime()))
|
|
51
|
+
throw new Error(`Invalid ISO datetime: ${input}`);
|
|
43
52
|
return d;
|
|
44
53
|
}
|
|
45
54
|
// No timezone => treat as UTC
|
|
46
|
-
const s = input.trim().replace(
|
|
55
|
+
const s = input.trim().replace(" ", "T");
|
|
47
56
|
const d = new Date(`${s}Z`);
|
|
48
|
-
if (Number.isNaN(d.getTime()))
|
|
57
|
+
if (Number.isNaN(d.getTime()))
|
|
58
|
+
throw new Error(`Invalid datetime (no tz): ${input}`);
|
|
49
59
|
return d;
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -55,10 +65,12 @@ function normalizeToUtcDate(input) {
|
|
|
55
65
|
// --- Purge helpers -----------------------------------------------------------
|
|
56
66
|
|
|
57
67
|
async function findPendingArticleJobs(article_id) {
|
|
58
|
-
const states = [
|
|
68
|
+
const states = ["delayed", "waiting", "waiting-children", "active"];
|
|
59
69
|
const jobs = await articlePublishQueue.getJobs(states);
|
|
60
70
|
return jobs.filter(
|
|
61
|
-
(j) =>
|
|
71
|
+
(j) =>
|
|
72
|
+
j?.name === "article:publish" &&
|
|
73
|
+
j?.data?.article_id === String(article_id)
|
|
62
74
|
);
|
|
63
75
|
}
|
|
64
76
|
|
|
@@ -72,7 +84,10 @@ async function purgeExistingForArticle(article_id) {
|
|
|
72
84
|
await j.remove();
|
|
73
85
|
removed++;
|
|
74
86
|
} catch (e) {
|
|
75
|
-
if (DEBUG)
|
|
87
|
+
if (DEBUG)
|
|
88
|
+
console.warn(
|
|
89
|
+
`[articlePublishQueue] failed to remove pending job ${j.id}: ${e.message}`
|
|
90
|
+
);
|
|
76
91
|
}
|
|
77
92
|
}
|
|
78
93
|
|
|
@@ -83,12 +98,15 @@ async function purgeExistingForArticle(article_id) {
|
|
|
83
98
|
await last.remove();
|
|
84
99
|
removed++;
|
|
85
100
|
} catch (e) {
|
|
86
|
-
if (DEBUG)
|
|
101
|
+
if (DEBUG)
|
|
102
|
+
console.warn(
|
|
103
|
+
`[articlePublishQueue] failed to remove stable job ${last.id}: ${e.message}`
|
|
104
|
+
);
|
|
87
105
|
}
|
|
88
106
|
}
|
|
89
107
|
|
|
90
108
|
if (DEBUG) {
|
|
91
|
-
console.info(
|
|
109
|
+
console.info("[articlePublishQueue] purgeExistingForArticle", {
|
|
92
110
|
article_id: String(article_id),
|
|
93
111
|
removed,
|
|
94
112
|
});
|
|
@@ -109,9 +127,16 @@ async function purgeExistingForArticle(article_id) {
|
|
|
109
127
|
* @param {object} [params.extra]
|
|
110
128
|
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
129
|
*/
|
|
112
|
-
async function addArticlePublishJob({
|
|
130
|
+
async function addArticlePublishJob({
|
|
131
|
+
article_id,
|
|
132
|
+
runAtUtc,
|
|
133
|
+
extra = {},
|
|
134
|
+
useStableId = false,
|
|
135
|
+
}) {
|
|
113
136
|
if (!article_id || !runAtUtc) {
|
|
114
|
-
throw new Error(
|
|
137
|
+
throw new Error(
|
|
138
|
+
"addArticlePublishJob: article_id and runAtUtc are required"
|
|
139
|
+
);
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
const runAt = normalizeToUtcDate(runAtUtc);
|
|
@@ -126,16 +151,18 @@ async function addArticlePublishJob({ article_id, runAtUtc, extra = {}, useStabl
|
|
|
126
151
|
await purgeExistingForArticle(article_id);
|
|
127
152
|
|
|
128
153
|
// Choose jobId strategy
|
|
129
|
-
const jobId = useStableId
|
|
154
|
+
const jobId = useStableId
|
|
155
|
+
? stableJobId(article_id)
|
|
156
|
+
: versionedJobId(article_id, runAtIso);
|
|
130
157
|
|
|
131
158
|
const job = await articlePublishQueue.add(
|
|
132
|
-
|
|
159
|
+
"article:publish",
|
|
133
160
|
{ article_id: String(article_id), runAtUtc: runAtIso, extra },
|
|
134
161
|
{ jobId, delay: delayMs }
|
|
135
162
|
);
|
|
136
163
|
|
|
137
164
|
if (DEBUG) {
|
|
138
|
-
console.info(
|
|
165
|
+
console.info("[articlePublishQueue] upsert", {
|
|
139
166
|
article_id: String(article_id),
|
|
140
167
|
jobId,
|
|
141
168
|
runAtIso,
|
|
@@ -161,4 +188,4 @@ module.exports = {
|
|
|
161
188
|
stableJobId,
|
|
162
189
|
versionedJobId,
|
|
163
190
|
normalizeToUtcDate,
|
|
164
|
-
};
|
|
191
|
+
};
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
// One-off *publish* scheduler for image album items (BullMQ).
|
|
3
3
|
// Strong upsert: purge any prior job for the same album before adding a new delayed job.
|
|
4
4
|
|
|
5
|
-
const { Queue } = require(
|
|
6
|
-
const redisClient = require(
|
|
5
|
+
const { Queue } = require("bullmq");
|
|
6
|
+
const redisClient = require("../redis/redisClient");
|
|
7
7
|
// const logger = require('../logger'); // uncomment if you have a shared logger
|
|
8
8
|
|
|
9
|
-
const DEBUG
|
|
9
|
+
const DEBUG = String(process.env.DEBUG_LOGGER || "").trim() === "1";
|
|
10
10
|
const DRIFT_MS = Math.max(0, Number(process.env.SCHEDULE_DRIFT_MS || 250));
|
|
11
11
|
|
|
12
|
-
const imageAlbumPublishQueue = new Queue(
|
|
12
|
+
const imageAlbumPublishQueue = new Queue("imageAlbumPublishQueue", {
|
|
13
13
|
connection: redisClient,
|
|
14
14
|
defaultJobOptions: {
|
|
15
15
|
attempts: 5,
|
|
16
|
-
backoff: { type:
|
|
16
|
+
backoff: { type: "exponential", delay: 2000 },
|
|
17
17
|
removeOnComplete: 500,
|
|
18
18
|
removeOnFail: 500,
|
|
19
19
|
},
|
|
@@ -21,11 +21,19 @@ const imageAlbumPublishQueue = new Queue('imageAlbumPublishQueue', {
|
|
|
21
21
|
|
|
22
22
|
// ── ID helpers ────────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
+
/** Replace invalid chars for BullMQ IDs */
|
|
25
|
+
function sanitizeId(str) {
|
|
26
|
+
return str.replace(/[:.]/g, "-");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Stable id (latest schedule) */
|
|
24
30
|
function stableJobId(album_id) {
|
|
25
|
-
return `imageAlbum:publish:${album_id}
|
|
31
|
+
return sanitizeId(`imageAlbum:publish:${album_id}`);
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Versioned id (keeps history distinct) */
|
|
27
35
|
function versionedJobId(album_id, runAtIso) {
|
|
28
|
-
return `imageAlbum:publish:${album_id}:${runAtIso}
|
|
36
|
+
return sanitizeId(`imageAlbum:publish:${album_id}:${runAtIso}`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
// ── Time helpers ──────────────────────────────────────────────────────────────
|
|
@@ -33,19 +41,21 @@ function versionedJobId(album_id, runAtIso) {
|
|
|
33
41
|
/** Parse anything into a UTC Date. Strings without timezone are treated as UTC. */
|
|
34
42
|
function normalizeToUtcDate(input) {
|
|
35
43
|
if (input instanceof Date) return new Date(input.getTime());
|
|
36
|
-
if (typeof input ===
|
|
44
|
+
if (typeof input === "number") return new Date(input); // epoch ms
|
|
37
45
|
|
|
38
|
-
if (typeof input ===
|
|
46
|
+
if (typeof input === "string") {
|
|
39
47
|
// Has timezone (Z or ±hh:mm)
|
|
40
48
|
if (/[zZ]|[+\-]\d{2}:\d{2}$/.test(input)) {
|
|
41
49
|
const d = new Date(input);
|
|
42
|
-
if (Number.isNaN(d.getTime()))
|
|
50
|
+
if (Number.isNaN(d.getTime()))
|
|
51
|
+
throw new Error(`Invalid ISO datetime: ${input}`);
|
|
43
52
|
return d;
|
|
44
53
|
}
|
|
45
54
|
// No timezone => treat as UTC
|
|
46
|
-
const s = input.trim().replace(
|
|
55
|
+
const s = input.trim().replace(" ", "T");
|
|
47
56
|
const d = new Date(`${s}Z`);
|
|
48
|
-
if (Number.isNaN(d.getTime()))
|
|
57
|
+
if (Number.isNaN(d.getTime()))
|
|
58
|
+
throw new Error(`Invalid datetime (no tz): ${input}`);
|
|
49
59
|
return d;
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -55,10 +65,11 @@ function normalizeToUtcDate(input) {
|
|
|
55
65
|
// ── Purge helpers ────────────────────────────────────────────────────────────
|
|
56
66
|
|
|
57
67
|
async function findPendingAlbumJobs(album_id) {
|
|
58
|
-
const states = [
|
|
68
|
+
const states = ["delayed", "waiting", "waiting-children", "active"];
|
|
59
69
|
const jobs = await imageAlbumPublishQueue.getJobs(states);
|
|
60
70
|
return jobs.filter(
|
|
61
|
-
(j) =>
|
|
71
|
+
(j) =>
|
|
72
|
+
j?.name === "imageAlbum:publish" && j?.data?.album_id === String(album_id)
|
|
62
73
|
);
|
|
63
74
|
}
|
|
64
75
|
|
|
@@ -72,7 +83,10 @@ async function purgeExistingForAlbum(album_id) {
|
|
|
72
83
|
await j.remove();
|
|
73
84
|
removed++;
|
|
74
85
|
} catch (e) {
|
|
75
|
-
if (DEBUG)
|
|
86
|
+
if (DEBUG)
|
|
87
|
+
console.warn(
|
|
88
|
+
`[imageAlbumPublishQueue] failed to remove pending job ${j.id}: ${e.message}`
|
|
89
|
+
);
|
|
76
90
|
}
|
|
77
91
|
}
|
|
78
92
|
|
|
@@ -83,12 +97,15 @@ async function purgeExistingForAlbum(album_id) {
|
|
|
83
97
|
await last.remove();
|
|
84
98
|
removed++;
|
|
85
99
|
} catch (e) {
|
|
86
|
-
if (DEBUG)
|
|
100
|
+
if (DEBUG)
|
|
101
|
+
console.warn(
|
|
102
|
+
`[imageAlbumPublishQueue] failed to remove stable job ${last.id}: ${e.message}`
|
|
103
|
+
);
|
|
87
104
|
}
|
|
88
105
|
}
|
|
89
106
|
|
|
90
107
|
if (DEBUG) {
|
|
91
|
-
console.info(
|
|
108
|
+
console.info("[imageAlbumPublishQueue] purgeExistingForAlbum", {
|
|
92
109
|
album_id: String(album_id),
|
|
93
110
|
removed,
|
|
94
111
|
});
|
|
@@ -109,9 +126,16 @@ async function purgeExistingForAlbum(album_id) {
|
|
|
109
126
|
* @param {object} [params.extra]
|
|
110
127
|
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
128
|
*/
|
|
112
|
-
async function addImageAlbumPublishJob({
|
|
129
|
+
async function addImageAlbumPublishJob({
|
|
130
|
+
album_id,
|
|
131
|
+
runAtUtc,
|
|
132
|
+
extra = {},
|
|
133
|
+
useStableId = false,
|
|
134
|
+
}) {
|
|
113
135
|
if (!album_id || !runAtUtc) {
|
|
114
|
-
throw new Error(
|
|
136
|
+
throw new Error(
|
|
137
|
+
"addImageAlbumPublishJob: album_id and runAtUtc are required"
|
|
138
|
+
);
|
|
115
139
|
}
|
|
116
140
|
|
|
117
141
|
const runAt = normalizeToUtcDate(runAtUtc);
|
|
@@ -126,16 +150,18 @@ async function addImageAlbumPublishJob({ album_id, runAtUtc, extra = {}, useStab
|
|
|
126
150
|
await purgeExistingForAlbum(album_id);
|
|
127
151
|
|
|
128
152
|
// Choose jobId strategy
|
|
129
|
-
const jobId = useStableId
|
|
153
|
+
const jobId = useStableId
|
|
154
|
+
? stableJobId(album_id)
|
|
155
|
+
: versionedJobId(album_id, runAtIso);
|
|
130
156
|
|
|
131
157
|
const job = await imageAlbumPublishQueue.add(
|
|
132
|
-
|
|
158
|
+
"imageAlbum:publish",
|
|
133
159
|
{ album_id: String(album_id), runAtUtc: runAtIso, extra },
|
|
134
160
|
{ jobId, delay: delayMs }
|
|
135
161
|
);
|
|
136
162
|
|
|
137
163
|
if (DEBUG) {
|
|
138
|
-
console.info(
|
|
164
|
+
console.info("[imageAlbumPublishQueue] upsert", {
|
|
139
165
|
album_id: String(album_id),
|
|
140
166
|
jobId,
|
|
141
167
|
runAtIso,
|
|
@@ -161,4 +187,4 @@ module.exports = {
|
|
|
161
187
|
stableJobId,
|
|
162
188
|
versionedJobId,
|
|
163
189
|
normalizeToUtcDate,
|
|
164
|
-
};
|
|
190
|
+
};
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
// One-off *publish* scheduler for image items (BullMQ).
|
|
3
3
|
// Strong upsert: purge any prior job for the same image before adding a new delayed job.
|
|
4
4
|
|
|
5
|
-
const { Queue } = require(
|
|
6
|
-
const redisClient = require(
|
|
5
|
+
const { Queue } = require("bullmq");
|
|
6
|
+
const redisClient = require("../redis/redisClient");
|
|
7
7
|
// const logger = require('../logger'); // uncomment if you have a shared logger
|
|
8
8
|
|
|
9
|
-
const DEBUG
|
|
9
|
+
const DEBUG = String(process.env.DEBUG_LOGGER || "").trim() === "1";
|
|
10
10
|
const DRIFT_MS = Math.max(0, Number(process.env.SCHEDULE_DRIFT_MS || 250));
|
|
11
11
|
|
|
12
|
-
const imagePublishQueue = new Queue(
|
|
12
|
+
const imagePublishQueue = new Queue("imagePublishQueue", {
|
|
13
13
|
connection: redisClient,
|
|
14
14
|
defaultJobOptions: {
|
|
15
15
|
attempts: 5,
|
|
16
|
-
backoff: { type:
|
|
16
|
+
backoff: { type: "exponential", delay: 2000 },
|
|
17
17
|
removeOnComplete: 500,
|
|
18
18
|
removeOnFail: 500,
|
|
19
19
|
},
|
|
@@ -21,11 +21,19 @@ const imagePublishQueue = new Queue('imagePublishQueue', {
|
|
|
21
21
|
|
|
22
22
|
// ── ID helpers ─────────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
+
/** Replace invalid chars for BullMQ IDs */
|
|
25
|
+
function sanitizeId(str) {
|
|
26
|
+
return str.replace(/[:.]/g, "-");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Stable id (latest schedule) */
|
|
24
30
|
function stableJobId(image_id) {
|
|
25
|
-
return `image:publish:${image_id}
|
|
31
|
+
return sanitizeId(`image:publish:${image_id}`);
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Versioned id (keeps history distinct) */
|
|
27
35
|
function versionedJobId(image_id, runAtIso) {
|
|
28
|
-
return `image:publish:${image_id}:${runAtIso}
|
|
36
|
+
return sanitizeId(`image:publish:${image_id}:${runAtIso}`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
// ── Time helpers ───────────────────────────────────────────────────────────────
|
|
@@ -33,19 +41,21 @@ function versionedJobId(image_id, runAtIso) {
|
|
|
33
41
|
/** Parse anything into a UTC Date. Strings without timezone are treated as UTC. */
|
|
34
42
|
function normalizeToUtcDate(input) {
|
|
35
43
|
if (input instanceof Date) return new Date(input.getTime());
|
|
36
|
-
if (typeof input ===
|
|
44
|
+
if (typeof input === "number") return new Date(input); // epoch ms
|
|
37
45
|
|
|
38
|
-
if (typeof input ===
|
|
46
|
+
if (typeof input === "string") {
|
|
39
47
|
// Has timezone (Z or ±hh:mm)
|
|
40
48
|
if (/[zZ]|[+\-]\d{2}:\d{2}$/.test(input)) {
|
|
41
49
|
const d = new Date(input);
|
|
42
|
-
if (Number.isNaN(d.getTime()))
|
|
50
|
+
if (Number.isNaN(d.getTime()))
|
|
51
|
+
throw new Error(`Invalid ISO datetime: ${input}`);
|
|
43
52
|
return d;
|
|
44
53
|
}
|
|
45
54
|
// No timezone => treat as UTC
|
|
46
|
-
const s = input.trim().replace(
|
|
55
|
+
const s = input.trim().replace(" ", "T");
|
|
47
56
|
const d = new Date(`${s}Z`);
|
|
48
|
-
if (Number.isNaN(d.getTime()))
|
|
57
|
+
if (Number.isNaN(d.getTime()))
|
|
58
|
+
throw new Error(`Invalid datetime (no tz): ${input}`);
|
|
49
59
|
return d;
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -55,10 +65,10 @@ function normalizeToUtcDate(input) {
|
|
|
55
65
|
// ── Purge helpers ─────────────────────────────────────────────────────────────
|
|
56
66
|
|
|
57
67
|
async function findPendingImageJobs(image_id) {
|
|
58
|
-
const states = [
|
|
68
|
+
const states = ["delayed", "waiting", "waiting-children", "active"];
|
|
59
69
|
const jobs = await imagePublishQueue.getJobs(states);
|
|
60
70
|
return jobs.filter(
|
|
61
|
-
(j) => j?.name ===
|
|
71
|
+
(j) => j?.name === "image:publish" && j?.data?.image_id === String(image_id)
|
|
62
72
|
);
|
|
63
73
|
}
|
|
64
74
|
|
|
@@ -72,7 +82,10 @@ async function purgeExistingForImage(image_id) {
|
|
|
72
82
|
await j.remove();
|
|
73
83
|
removed++;
|
|
74
84
|
} catch (e) {
|
|
75
|
-
if (DEBUG)
|
|
85
|
+
if (DEBUG)
|
|
86
|
+
console.warn(
|
|
87
|
+
`[imagePublishQueue] failed to remove pending job ${j.id}: ${e.message}`
|
|
88
|
+
);
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
|
|
@@ -83,12 +96,15 @@ async function purgeExistingForImage(image_id) {
|
|
|
83
96
|
await last.remove();
|
|
84
97
|
removed++;
|
|
85
98
|
} catch (e) {
|
|
86
|
-
if (DEBUG)
|
|
99
|
+
if (DEBUG)
|
|
100
|
+
console.warn(
|
|
101
|
+
`[imagePublishQueue] failed to remove stable job ${last.id}: ${e.message}`
|
|
102
|
+
);
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
if (DEBUG) {
|
|
91
|
-
console.info(
|
|
107
|
+
console.info("[imagePublishQueue] purgeExistingForImage", {
|
|
92
108
|
image_id: String(image_id),
|
|
93
109
|
removed,
|
|
94
110
|
});
|
|
@@ -109,9 +125,14 @@ async function purgeExistingForImage(image_id) {
|
|
|
109
125
|
* @param {object} [params.extra]
|
|
110
126
|
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
127
|
*/
|
|
112
|
-
async function addImagePublishJob({
|
|
128
|
+
async function addImagePublishJob({
|
|
129
|
+
image_id,
|
|
130
|
+
runAtUtc,
|
|
131
|
+
extra = {},
|
|
132
|
+
useStableId = false,
|
|
133
|
+
}) {
|
|
113
134
|
if (!image_id || !runAtUtc) {
|
|
114
|
-
throw new Error(
|
|
135
|
+
throw new Error("addImagePublishJob: image_id and runAtUtc are required");
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
const runAt = normalizeToUtcDate(runAtUtc);
|
|
@@ -126,16 +147,18 @@ async function addImagePublishJob({ image_id, runAtUtc, extra = {}, useStableId
|
|
|
126
147
|
await purgeExistingForImage(image_id);
|
|
127
148
|
|
|
128
149
|
// Choose jobId strategy
|
|
129
|
-
const jobId = useStableId
|
|
150
|
+
const jobId = useStableId
|
|
151
|
+
? stableJobId(image_id)
|
|
152
|
+
: versionedJobId(image_id, runAtIso);
|
|
130
153
|
|
|
131
154
|
const job = await imagePublishQueue.add(
|
|
132
|
-
|
|
155
|
+
"image:publish",
|
|
133
156
|
{ image_id: String(image_id), runAtUtc: runAtIso, extra },
|
|
134
157
|
{ jobId, delay: delayMs }
|
|
135
158
|
);
|
|
136
159
|
|
|
137
160
|
if (DEBUG) {
|
|
138
|
-
console.info(
|
|
161
|
+
console.info("[imagePublishQueue] upsert", {
|
|
139
162
|
image_id: String(image_id),
|
|
140
163
|
jobId,
|
|
141
164
|
runAtIso,
|
|
@@ -161,4 +184,4 @@ module.exports = {
|
|
|
161
184
|
stableJobId,
|
|
162
185
|
versionedJobId,
|
|
163
186
|
normalizeToUtcDate,
|
|
164
|
-
};
|
|
187
|
+
};
|
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
// One-off *publish* scheduler for news items (BullMQ).
|
|
3
3
|
// Strong upsert: purge any prior job for the same news before adding a new delayed job.
|
|
4
4
|
|
|
5
|
-
const { Queue } = require(
|
|
6
|
-
const redisClient = require(
|
|
5
|
+
const { Queue } = require("bullmq");
|
|
6
|
+
const redisClient = require("../redis/redisClient");
|
|
7
7
|
// const logger = require('../logger'); // optional
|
|
8
8
|
|
|
9
|
-
const DEBUG
|
|
9
|
+
const DEBUG = String(process.env.DEBUG_LOGGER || "").trim() === "1";
|
|
10
10
|
const DRIFT_MS = Math.max(0, Number(process.env.SCHEDULE_DRIFT_MS || 250));
|
|
11
11
|
|
|
12
|
-
const newsPublishQueue = new Queue(
|
|
12
|
+
const newsPublishQueue = new Queue("newsPublishQueue", {
|
|
13
13
|
connection: redisClient,
|
|
14
14
|
defaultJobOptions: {
|
|
15
15
|
attempts: 5,
|
|
16
|
-
backoff: { type:
|
|
16
|
+
backoff: { type: "exponential", delay: 2000 },
|
|
17
17
|
removeOnComplete: 500,
|
|
18
18
|
removeOnFail: 500,
|
|
19
19
|
},
|
|
@@ -21,11 +21,19 @@ const newsPublishQueue = new Queue('newsPublishQueue', {
|
|
|
21
21
|
|
|
22
22
|
// ── ID helpers ────────────────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
+
/** Replace invalid chars for BullMQ IDs */
|
|
25
|
+
function sanitizeId(str) {
|
|
26
|
+
return str.replace(/[:.]/g, "-");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Stable id (latest schedule) */
|
|
24
30
|
function stableJobId(news_id) {
|
|
25
|
-
return `news:publish:${news_id}
|
|
31
|
+
return sanitizeId(`news:publish:${news_id}`);
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Versioned id (keeps history distinct) */
|
|
27
35
|
function versionedJobId(news_id, runAtIso) {
|
|
28
|
-
return `news:publish:${news_id}:${runAtIso}
|
|
36
|
+
return sanitizeId(`news:publish:${news_id}:${runAtIso}`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
// ── Time helpers ──────────────────────────────────────────────────────────────
|
|
@@ -33,19 +41,21 @@ function versionedJobId(news_id, runAtIso) {
|
|
|
33
41
|
/** Parse anything into a UTC Date. Strings without timezone are treated as UTC. */
|
|
34
42
|
function normalizeToUtcDate(input) {
|
|
35
43
|
if (input instanceof Date) return new Date(input.getTime());
|
|
36
|
-
if (typeof input ===
|
|
44
|
+
if (typeof input === "number") return new Date(input); // epoch ms
|
|
37
45
|
|
|
38
|
-
if (typeof input ===
|
|
46
|
+
if (typeof input === "string") {
|
|
39
47
|
// Has timezone (Z or ±hh:mm)
|
|
40
48
|
if (/[zZ]|[+\-]\d{2}:\d{2}$/.test(input)) {
|
|
41
49
|
const d = new Date(input);
|
|
42
|
-
if (Number.isNaN(d.getTime()))
|
|
50
|
+
if (Number.isNaN(d.getTime()))
|
|
51
|
+
throw new Error(`Invalid ISO datetime: ${input}`);
|
|
43
52
|
return d;
|
|
44
53
|
}
|
|
45
54
|
// No timezone => treat as UTC
|
|
46
|
-
const s = input.trim().replace(
|
|
55
|
+
const s = input.trim().replace(" ", "T");
|
|
47
56
|
const d = new Date(`${s}Z`);
|
|
48
|
-
if (Number.isNaN(d.getTime()))
|
|
57
|
+
if (Number.isNaN(d.getTime()))
|
|
58
|
+
throw new Error(`Invalid datetime (no tz): ${input}`);
|
|
49
59
|
return d;
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -55,10 +65,10 @@ function normalizeToUtcDate(input) {
|
|
|
55
65
|
// ── Purge helpers ────────────────────────────────────────────────────────────
|
|
56
66
|
|
|
57
67
|
async function findPendingNewsJobs(news_id) {
|
|
58
|
-
const states = [
|
|
68
|
+
const states = ["delayed", "waiting", "waiting-children", "active"];
|
|
59
69
|
const jobs = await newsPublishQueue.getJobs(states);
|
|
60
70
|
return jobs.filter(
|
|
61
|
-
(j) => j?.name ===
|
|
71
|
+
(j) => j?.name === "news:publish" && j?.data?.news_id === String(news_id)
|
|
62
72
|
);
|
|
63
73
|
}
|
|
64
74
|
|
|
@@ -72,7 +82,10 @@ async function purgeExistingForNews(news_id) {
|
|
|
72
82
|
await j.remove();
|
|
73
83
|
removed++;
|
|
74
84
|
} catch (e) {
|
|
75
|
-
if (DEBUG)
|
|
85
|
+
if (DEBUG)
|
|
86
|
+
console.warn(
|
|
87
|
+
`[newsPublishQueue] failed to remove pending job ${j.id}: ${e.message}`
|
|
88
|
+
);
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
|
|
@@ -83,12 +96,15 @@ async function purgeExistingForNews(news_id) {
|
|
|
83
96
|
await last.remove();
|
|
84
97
|
removed++;
|
|
85
98
|
} catch (e) {
|
|
86
|
-
if (DEBUG)
|
|
99
|
+
if (DEBUG)
|
|
100
|
+
console.warn(
|
|
101
|
+
`[newsPublishQueue] failed to remove stable job ${last.id}: ${e.message}`
|
|
102
|
+
);
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
105
|
|
|
90
106
|
if (DEBUG) {
|
|
91
|
-
console.info(
|
|
107
|
+
console.info("[newsPublishQueue] purgeExistingForNews", {
|
|
92
108
|
news_id: String(news_id),
|
|
93
109
|
removed,
|
|
94
110
|
});
|
|
@@ -109,9 +125,14 @@ async function purgeExistingForNews(news_id) {
|
|
|
109
125
|
* @param {object} [params.extra]
|
|
110
126
|
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
127
|
*/
|
|
112
|
-
async function addNewsPublishJob({
|
|
128
|
+
async function addNewsPublishJob({
|
|
129
|
+
news_id,
|
|
130
|
+
runAtUtc,
|
|
131
|
+
extra = {},
|
|
132
|
+
useStableId = false,
|
|
133
|
+
}) {
|
|
113
134
|
if (!news_id || !runAtUtc) {
|
|
114
|
-
throw new Error(
|
|
135
|
+
throw new Error("addNewsPublishJob: news_id and runAtUtc are required");
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
const runAt = normalizeToUtcDate(runAtUtc);
|
|
@@ -126,16 +147,18 @@ async function addNewsPublishJob({ news_id, runAtUtc, extra = {}, useStableId =
|
|
|
126
147
|
await purgeExistingForNews(news_id);
|
|
127
148
|
|
|
128
149
|
// Choose jobId strategy
|
|
129
|
-
const jobId = useStableId
|
|
150
|
+
const jobId = useStableId
|
|
151
|
+
? stableJobId(news_id)
|
|
152
|
+
: versionedJobId(news_id, runAtIso);
|
|
130
153
|
|
|
131
154
|
const job = await newsPublishQueue.add(
|
|
132
|
-
|
|
155
|
+
"news:publish",
|
|
133
156
|
{ news_id: String(news_id), runAtUtc: runAtIso, extra },
|
|
134
157
|
{ jobId, delay: delayMs }
|
|
135
158
|
);
|
|
136
159
|
|
|
137
160
|
if (DEBUG) {
|
|
138
|
-
console.info(
|
|
161
|
+
console.info("[newsPublishQueue] upsert", {
|
|
139
162
|
news_id: String(news_id),
|
|
140
163
|
jobId,
|
|
141
164
|
runAtIso,
|
|
@@ -161,4 +184,4 @@ module.exports = {
|
|
|
161
184
|
stableJobId,
|
|
162
185
|
versionedJobId,
|
|
163
186
|
normalizeToUtcDate,
|
|
164
|
-
};
|
|
187
|
+
};
|