@yellowpanther/shared 1.2.6 → 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 +1 -1
- 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
|
@@ -2,18 +2,18 @@
|
|
|
2
2
|
// One-off *publish* scheduler for page items (BullMQ).
|
|
3
3
|
// Strong upsert: purge any prior job for the same page 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 pagePublishQueue = new Queue(
|
|
12
|
+
const pagePublishQueue = new Queue("pagePublishQueue", {
|
|
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 pagePublishQueue = new Queue('pagePublishQueue', {
|
|
|
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(page_id) {
|
|
25
|
-
return `page:publish:${page_id}
|
|
31
|
+
return sanitizeId(`page:publish:${page_id}`);
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Versioned id (keeps history distinct) */
|
|
27
35
|
function versionedJobId(page_id, runAtIso) {
|
|
28
|
-
return `page:publish:${page_id}:${runAtIso}
|
|
36
|
+
return sanitizeId(`page:publish:${page_id}:${runAtIso}`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
// ── Time helpers ───────────────────────────────────────────────────────────────
|
|
@@ -33,19 +41,21 @@ function versionedJobId(page_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 findPendingPageJobs(page_id) {
|
|
58
|
-
const states = [
|
|
68
|
+
const states = ["delayed", "waiting", "waiting-children", "active"];
|
|
59
69
|
const jobs = await pagePublishQueue.getJobs(states);
|
|
60
70
|
return jobs.filter(
|
|
61
|
-
(j) => j?.name ===
|
|
71
|
+
(j) => j?.name === "page:publish" && j?.data?.page_id === String(page_id)
|
|
62
72
|
);
|
|
63
73
|
}
|
|
64
74
|
|
|
@@ -72,7 +82,10 @@ async function purgeExistingForPage(page_id) {
|
|
|
72
82
|
await j.remove();
|
|
73
83
|
removed++;
|
|
74
84
|
} catch (e) {
|
|
75
|
-
if (DEBUG)
|
|
85
|
+
if (DEBUG)
|
|
86
|
+
console.warn(
|
|
87
|
+
`[pagePublishQueue] failed to remove pending job ${j.id}: ${e.message}`
|
|
88
|
+
);
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
|
|
@@ -83,12 +96,15 @@ async function purgeExistingForPage(page_id) {
|
|
|
83
96
|
await last.remove();
|
|
84
97
|
removed++;
|
|
85
98
|
} catch (e) {
|
|
86
|
-
if (DEBUG)
|
|
99
|
+
if (DEBUG)
|
|
100
|
+
console.warn(
|
|
101
|
+
`[pagePublishQueue] 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("[pagePublishQueue] purgeExistingForPage", {
|
|
92
108
|
page_id: String(page_id),
|
|
93
109
|
removed,
|
|
94
110
|
});
|
|
@@ -109,9 +125,14 @@ async function purgeExistingForPage(page_id) {
|
|
|
109
125
|
* @param {object} [params.extra]
|
|
110
126
|
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
127
|
*/
|
|
112
|
-
async function addPagePublishJob({
|
|
128
|
+
async function addPagePublishJob({
|
|
129
|
+
page_id,
|
|
130
|
+
runAtUtc,
|
|
131
|
+
extra = {},
|
|
132
|
+
useStableId = false,
|
|
133
|
+
}) {
|
|
113
134
|
if (!page_id || !runAtUtc) {
|
|
114
|
-
throw new Error(
|
|
135
|
+
throw new Error("addPagePublishJob: page_id and runAtUtc are required");
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
const runAt = normalizeToUtcDate(runAtUtc);
|
|
@@ -126,16 +147,18 @@ async function addPagePublishJob({ page_id, runAtUtc, extra = {}, useStableId =
|
|
|
126
147
|
await purgeExistingForPage(page_id);
|
|
127
148
|
|
|
128
149
|
// Choose jobId strategy
|
|
129
|
-
const jobId = useStableId
|
|
150
|
+
const jobId = useStableId
|
|
151
|
+
? stableJobId(page_id)
|
|
152
|
+
: versionedJobId(page_id, runAtIso);
|
|
130
153
|
|
|
131
154
|
const job = await pagePublishQueue.add(
|
|
132
|
-
|
|
155
|
+
"page:publish",
|
|
133
156
|
{ page_id: String(page_id), runAtUtc: runAtIso, extra },
|
|
134
157
|
{ jobId, delay: delayMs }
|
|
135
158
|
);
|
|
136
159
|
|
|
137
160
|
if (DEBUG) {
|
|
138
|
-
console.info(
|
|
161
|
+
console.info("[pagePublishQueue] upsert", {
|
|
139
162
|
page_id: String(page_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 prediction items (BullMQ).
|
|
3
3
|
// Strong upsert: purge any prior job for the same prediction 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 predictionPublishQueue = new Queue(
|
|
12
|
+
const predictionPublishQueue = new Queue("predictionPublishQueue", {
|
|
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 predictionPublishQueue = new Queue('predictionPublishQueue', {
|
|
|
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(prediction_id) {
|
|
25
|
-
return `prediction:publish:${prediction_id}
|
|
31
|
+
return sanitizeId(`prediction:publish:${prediction_id}`);
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Versioned id (keeps history distinct) */
|
|
27
35
|
function versionedJobId(prediction_id, runAtIso) {
|
|
28
|
-
return `prediction:publish:${prediction_id}:${runAtIso}
|
|
36
|
+
return sanitizeId(`prediction:publish:${prediction_id}:${runAtIso}`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
// ── Time helpers ───────────────────────────────────────────────────────────────
|
|
@@ -33,19 +41,21 @@ function versionedJobId(prediction_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 findPendingPredictionJobs(prediction_id) {
|
|
58
|
-
const states = [
|
|
68
|
+
const states = ["delayed", "waiting", "waiting-children", "active"];
|
|
59
69
|
const jobs = await predictionPublishQueue.getJobs(states);
|
|
60
70
|
return jobs.filter(
|
|
61
|
-
(j) =>
|
|
71
|
+
(j) =>
|
|
72
|
+
j?.name === "prediction:publish" &&
|
|
73
|
+
j?.data?.prediction_id === String(prediction_id)
|
|
62
74
|
);
|
|
63
75
|
}
|
|
64
76
|
|
|
@@ -72,7 +84,10 @@ async function purgeExistingForPrediction(prediction_id) {
|
|
|
72
84
|
await j.remove();
|
|
73
85
|
removed++;
|
|
74
86
|
} catch (e) {
|
|
75
|
-
if (DEBUG)
|
|
87
|
+
if (DEBUG)
|
|
88
|
+
console.warn(
|
|
89
|
+
`[predictionPublishQueue] failed to remove pending job ${j.id}: ${e.message}`
|
|
90
|
+
);
|
|
76
91
|
}
|
|
77
92
|
}
|
|
78
93
|
|
|
@@ -83,12 +98,15 @@ async function purgeExistingForPrediction(prediction_id) {
|
|
|
83
98
|
await last.remove();
|
|
84
99
|
removed++;
|
|
85
100
|
} catch (e) {
|
|
86
|
-
if (DEBUG)
|
|
101
|
+
if (DEBUG)
|
|
102
|
+
console.warn(
|
|
103
|
+
`[predictionPublishQueue] 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("[predictionPublishQueue] purgeExistingForPrediction", {
|
|
92
110
|
prediction_id: String(prediction_id),
|
|
93
111
|
removed,
|
|
94
112
|
});
|
|
@@ -109,9 +127,16 @@ async function purgeExistingForPrediction(prediction_id) {
|
|
|
109
127
|
* @param {object} [params.extra]
|
|
110
128
|
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
129
|
*/
|
|
112
|
-
async function addPredictionPublishJob({
|
|
130
|
+
async function addPredictionPublishJob({
|
|
131
|
+
prediction_id,
|
|
132
|
+
runAtUtc,
|
|
133
|
+
extra = {},
|
|
134
|
+
useStableId = false,
|
|
135
|
+
}) {
|
|
113
136
|
if (!prediction_id || !runAtUtc) {
|
|
114
|
-
throw new Error(
|
|
137
|
+
throw new Error(
|
|
138
|
+
"addPredictionPublishJob: prediction_id and runAtUtc are required"
|
|
139
|
+
);
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
const runAt = normalizeToUtcDate(runAtUtc);
|
|
@@ -126,16 +151,18 @@ async function addPredictionPublishJob({ prediction_id, runAtUtc, extra = {}, us
|
|
|
126
151
|
await purgeExistingForPrediction(prediction_id);
|
|
127
152
|
|
|
128
153
|
// Choose jobId strategy
|
|
129
|
-
const jobId = useStableId
|
|
154
|
+
const jobId = useStableId
|
|
155
|
+
? stableJobId(prediction_id)
|
|
156
|
+
: versionedJobId(prediction_id, runAtIso);
|
|
130
157
|
|
|
131
158
|
const job = await predictionPublishQueue.add(
|
|
132
|
-
|
|
159
|
+
"prediction:publish",
|
|
133
160
|
{ prediction_id: String(prediction_id), runAtUtc: runAtIso, extra },
|
|
134
161
|
{ jobId, delay: delayMs }
|
|
135
162
|
);
|
|
136
163
|
|
|
137
164
|
if (DEBUG) {
|
|
138
|
-
console.info(
|
|
165
|
+
console.info("[predictionPublishQueue] upsert", {
|
|
139
166
|
prediction_id: String(prediction_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 product items (BullMQ).
|
|
3
3
|
// Strong upsert: purge any prior job for the same product 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 productPublishQueue = new Queue(
|
|
12
|
+
const productPublishQueue = new Queue("productPublishQueue", {
|
|
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 productPublishQueue = new Queue('productPublishQueue', {
|
|
|
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(product_id) {
|
|
25
|
-
return `product:publish:${product_id}
|
|
31
|
+
return sanitizeId(`product:publish:${product_id}`);
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Versioned id (keeps history distinct) */
|
|
27
35
|
function versionedJobId(product_id, runAtIso) {
|
|
28
|
-
return `product:publish:${product_id}:${runAtIso}
|
|
36
|
+
return sanitizeId(`product:publish:${product_id}:${runAtIso}`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
// ── Time helpers ───────────────────────────────────────────────────────────────
|
|
@@ -33,19 +41,21 @@ function versionedJobId(product_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 findPendingProductJobs(product_id) {
|
|
58
|
-
const states = [
|
|
68
|
+
const states = ["delayed", "waiting", "waiting-children", "active"];
|
|
59
69
|
const jobs = await productPublishQueue.getJobs(states);
|
|
60
70
|
return jobs.filter(
|
|
61
|
-
(j) =>
|
|
71
|
+
(j) =>
|
|
72
|
+
j?.name === "product:publish" &&
|
|
73
|
+
j?.data?.product_id === String(product_id)
|
|
62
74
|
);
|
|
63
75
|
}
|
|
64
76
|
|
|
@@ -72,7 +84,10 @@ async function purgeExistingForProduct(product_id) {
|
|
|
72
84
|
await j.remove();
|
|
73
85
|
removed++;
|
|
74
86
|
} catch (e) {
|
|
75
|
-
if (DEBUG)
|
|
87
|
+
if (DEBUG)
|
|
88
|
+
console.warn(
|
|
89
|
+
`[productPublishQueue] failed to remove pending job ${j.id}: ${e.message}`
|
|
90
|
+
);
|
|
76
91
|
}
|
|
77
92
|
}
|
|
78
93
|
|
|
@@ -83,12 +98,15 @@ async function purgeExistingForProduct(product_id) {
|
|
|
83
98
|
await last.remove();
|
|
84
99
|
removed++;
|
|
85
100
|
} catch (e) {
|
|
86
|
-
if (DEBUG)
|
|
101
|
+
if (DEBUG)
|
|
102
|
+
console.warn(
|
|
103
|
+
`[productPublishQueue] 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("[productPublishQueue] purgeExistingForProduct", {
|
|
92
110
|
product_id: String(product_id),
|
|
93
111
|
removed,
|
|
94
112
|
});
|
|
@@ -109,9 +127,16 @@ async function purgeExistingForProduct(product_id) {
|
|
|
109
127
|
* @param {object} [params.extra]
|
|
110
128
|
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
129
|
*/
|
|
112
|
-
async function addProductPublishJob({
|
|
130
|
+
async function addProductPublishJob({
|
|
131
|
+
product_id,
|
|
132
|
+
runAtUtc,
|
|
133
|
+
extra = {},
|
|
134
|
+
useStableId = false,
|
|
135
|
+
}) {
|
|
113
136
|
if (!product_id || !runAtUtc) {
|
|
114
|
-
throw new Error(
|
|
137
|
+
throw new Error(
|
|
138
|
+
"addProductPublishJob: product_id and runAtUtc are required"
|
|
139
|
+
);
|
|
115
140
|
}
|
|
116
141
|
|
|
117
142
|
const runAt = normalizeToUtcDate(runAtUtc);
|
|
@@ -126,16 +151,18 @@ async function addProductPublishJob({ product_id, runAtUtc, extra = {}, useStabl
|
|
|
126
151
|
await purgeExistingForProduct(product_id);
|
|
127
152
|
|
|
128
153
|
// Choose jobId strategy
|
|
129
|
-
const jobId = useStableId
|
|
154
|
+
const jobId = useStableId
|
|
155
|
+
? stableJobId(product_id)
|
|
156
|
+
: versionedJobId(product_id, runAtIso);
|
|
130
157
|
|
|
131
158
|
const job = await productPublishQueue.add(
|
|
132
|
-
|
|
159
|
+
"product:publish",
|
|
133
160
|
{ product_id: String(product_id), runAtUtc: runAtIso, extra },
|
|
134
161
|
{ jobId, delay: delayMs }
|
|
135
162
|
);
|
|
136
163
|
|
|
137
164
|
if (DEBUG) {
|
|
138
|
-
console.info(
|
|
165
|
+
console.info("[productPublishQueue] upsert", {
|
|
139
166
|
product_id: String(product_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 quiz items (BullMQ).
|
|
3
3
|
// Strong upsert: purge any prior job for the same quiz 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 quizPublishQueue = new Queue(
|
|
12
|
+
const quizPublishQueue = new Queue("quizPublishQueue", {
|
|
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 quizPublishQueue = new Queue('quizPublishQueue', {
|
|
|
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(quiz_id) {
|
|
25
|
-
return `quiz:publish:${quiz_id}
|
|
31
|
+
return sanitizeId(`quiz:publish:${quiz_id}`);
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
/** Versioned id (keeps history distinct) */
|
|
27
35
|
function versionedJobId(quiz_id, runAtIso) {
|
|
28
|
-
return `quiz:publish:${quiz_id}:${runAtIso}
|
|
36
|
+
return sanitizeId(`quiz:publish:${quiz_id}:${runAtIso}`);
|
|
29
37
|
}
|
|
30
38
|
|
|
31
39
|
// ── Time helpers ───────────────────────────────────────────────────────────────
|
|
@@ -33,19 +41,21 @@ function versionedJobId(quiz_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 findPendingQuizJobs(quiz_id) {
|
|
58
|
-
const states = [
|
|
68
|
+
const states = ["delayed", "waiting", "waiting-children", "active"];
|
|
59
69
|
const jobs = await quizPublishQueue.getJobs(states);
|
|
60
70
|
return jobs.filter(
|
|
61
|
-
(j) => j?.name ===
|
|
71
|
+
(j) => j?.name === "quiz:publish" && j?.data?.quiz_id === String(quiz_id)
|
|
62
72
|
);
|
|
63
73
|
}
|
|
64
74
|
|
|
@@ -72,7 +82,10 @@ async function purgeExistingForQuiz(quiz_id) {
|
|
|
72
82
|
await j.remove();
|
|
73
83
|
removed++;
|
|
74
84
|
} catch (e) {
|
|
75
|
-
if (DEBUG)
|
|
85
|
+
if (DEBUG)
|
|
86
|
+
console.warn(
|
|
87
|
+
`[quizPublishQueue] failed to remove pending job ${j.id}: ${e.message}`
|
|
88
|
+
);
|
|
76
89
|
}
|
|
77
90
|
}
|
|
78
91
|
|
|
@@ -83,12 +96,15 @@ async function purgeExistingForQuiz(quiz_id) {
|
|
|
83
96
|
await last.remove();
|
|
84
97
|
removed++;
|
|
85
98
|
} catch (e) {
|
|
86
|
-
if (DEBUG)
|
|
99
|
+
if (DEBUG)
|
|
100
|
+
console.warn(
|
|
101
|
+
`[quizPublishQueue] 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("[quizPublishQueue] purgeExistingForQuiz", {
|
|
92
108
|
quiz_id: String(quiz_id),
|
|
93
109
|
removed,
|
|
94
110
|
});
|
|
@@ -109,9 +125,14 @@ async function purgeExistingForQuiz(quiz_id) {
|
|
|
109
125
|
* @param {object} [params.extra]
|
|
110
126
|
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
127
|
*/
|
|
112
|
-
async function addQuizPublishJob({
|
|
128
|
+
async function addQuizPublishJob({
|
|
129
|
+
quiz_id,
|
|
130
|
+
runAtUtc,
|
|
131
|
+
extra = {},
|
|
132
|
+
useStableId = false,
|
|
133
|
+
}) {
|
|
113
134
|
if (!quiz_id || !runAtUtc) {
|
|
114
|
-
throw new Error(
|
|
135
|
+
throw new Error("addQuizPublishJob: quiz_id and runAtUtc are required");
|
|
115
136
|
}
|
|
116
137
|
|
|
117
138
|
const runAt = normalizeToUtcDate(runAtUtc);
|
|
@@ -126,16 +147,18 @@ async function addQuizPublishJob({ quiz_id, runAtUtc, extra = {}, useStableId =
|
|
|
126
147
|
await purgeExistingForQuiz(quiz_id);
|
|
127
148
|
|
|
128
149
|
// Choose jobId strategy
|
|
129
|
-
const jobId = useStableId
|
|
150
|
+
const jobId = useStableId
|
|
151
|
+
? stableJobId(quiz_id)
|
|
152
|
+
: versionedJobId(quiz_id, runAtIso);
|
|
130
153
|
|
|
131
154
|
const job = await quizPublishQueue.add(
|
|
132
|
-
|
|
155
|
+
"quiz:publish",
|
|
133
156
|
{ quiz_id: String(quiz_id), runAtUtc: runAtIso, extra },
|
|
134
157
|
{ jobId, delay: delayMs }
|
|
135
158
|
);
|
|
136
159
|
|
|
137
160
|
if (DEBUG) {
|
|
138
|
-
console.info(
|
|
161
|
+
console.info("[quizPublishQueue] upsert", {
|
|
139
162
|
quiz_id: String(quiz_id),
|
|
140
163
|
jobId,
|
|
141
164
|
runAtIso,
|
|
@@ -161,4 +184,4 @@ module.exports = {
|
|
|
161
184
|
stableJobId,
|
|
162
185
|
versionedJobId,
|
|
163
186
|
normalizeToUtcDate,
|
|
164
|
-
};
|
|
187
|
+
};
|