@yellowpanther/shared 1.1.7 → 1.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/package.json +12 -1
- package/src/config/index.js +8 -0
- package/src/index.js +10 -1
- package/src/queue/articlePublishQueue.js +164 -0
- package/src/queue/imagePublishQueue.js +164 -0
- package/src/queue/newsPublishQueue.js +164 -0
- package/src/queue/pagePublishQueue.js +164 -0
- package/src/queue/predictionPublishQueue.js +164 -0
- package/src/queue/productPublishQueue.js +164 -0
- package/src/queue/queueRegistry.js +40 -0
- package/src/queue/quizPublishQueue.js +164 -0
- package/src/queue/videoPublishQueue.js +164 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// src/queue/pagePublishQueue.js
|
|
2
|
+
// One-off *publish* scheduler for page items (BullMQ).
|
|
3
|
+
// Strong upsert: purge any prior job for the same page before adding a new delayed job.
|
|
4
|
+
|
|
5
|
+
const { Queue } = require('bullmq');
|
|
6
|
+
const redisClient = require('../redis/redisClient');
|
|
7
|
+
// const logger = require('../logger'); // uncomment if you have a shared logger
|
|
8
|
+
|
|
9
|
+
const DEBUG = String(process.env.DEBUG_LOGGER || '').trim() === '1';
|
|
10
|
+
const DRIFT_MS = Math.max(0, Number(process.env.SCHEDULE_DRIFT_MS || 250));
|
|
11
|
+
|
|
12
|
+
const pagePublishQueue = new Queue('pagePublishQueue', {
|
|
13
|
+
connection: redisClient,
|
|
14
|
+
defaultJobOptions: {
|
|
15
|
+
attempts: 5,
|
|
16
|
+
backoff: { type: 'exponential', delay: 2000 },
|
|
17
|
+
removeOnComplete: 500,
|
|
18
|
+
removeOnFail: 500,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ── ID helpers ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function stableJobId(page_id) {
|
|
25
|
+
return `page:publish:${page_id}`;
|
|
26
|
+
}
|
|
27
|
+
function versionedJobId(page_id, runAtIso) {
|
|
28
|
+
return `page:publish:${page_id}:${runAtIso}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Time helpers ───────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** Parse anything into a UTC Date. Strings without timezone are treated as UTC. */
|
|
34
|
+
function normalizeToUtcDate(input) {
|
|
35
|
+
if (input instanceof Date) return new Date(input.getTime());
|
|
36
|
+
if (typeof input === 'number') return new Date(input); // epoch ms
|
|
37
|
+
|
|
38
|
+
if (typeof input === 'string') {
|
|
39
|
+
// Has timezone (Z or ±hh:mm)
|
|
40
|
+
if (/[zZ]|[+\-]\d{2}:\d{2}$/.test(input)) {
|
|
41
|
+
const d = new Date(input);
|
|
42
|
+
if (Number.isNaN(d.getTime())) throw new Error(`Invalid ISO datetime: ${input}`);
|
|
43
|
+
return d;
|
|
44
|
+
}
|
|
45
|
+
// No timezone => treat as UTC
|
|
46
|
+
const s = input.trim().replace(' ', 'T');
|
|
47
|
+
const d = new Date(`${s}Z`);
|
|
48
|
+
if (Number.isNaN(d.getTime())) throw new Error(`Invalid datetime (no tz): ${input}`);
|
|
49
|
+
return d;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error(`Unsupported runAtUtc type: ${typeof input}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Purge helpers ─────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
async function findPendingPageJobs(page_id) {
|
|
58
|
+
const states = ['delayed', 'waiting', 'waiting-children', 'active'];
|
|
59
|
+
const jobs = await pagePublishQueue.getJobs(states);
|
|
60
|
+
return jobs.filter(
|
|
61
|
+
(j) => j?.name === 'page:publish' && j?.data?.page_id === String(page_id)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function purgeExistingForPage(page_id) {
|
|
66
|
+
let removed = 0;
|
|
67
|
+
|
|
68
|
+
// Remove any pending/waiting/active jobs for this page
|
|
69
|
+
const pendings = await findPendingPageJobs(page_id);
|
|
70
|
+
for (const j of pendings) {
|
|
71
|
+
try {
|
|
72
|
+
await j.remove();
|
|
73
|
+
removed++;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (DEBUG) console.warn(`[pagePublishQueue] failed to remove pending job ${j.id}: ${e.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Also remove a leftover stable job if present
|
|
80
|
+
const last = await pagePublishQueue.getJob(stableJobId(page_id));
|
|
81
|
+
if (last) {
|
|
82
|
+
try {
|
|
83
|
+
await last.remove();
|
|
84
|
+
removed++;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (DEBUG) console.warn(`[pagePublishQueue] failed to remove stable job ${last.id}: ${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (DEBUG) {
|
|
91
|
+
console.info('[pagePublishQueue] purgeExistingForPage', {
|
|
92
|
+
page_id: String(page_id),
|
|
93
|
+
removed,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Upsert a one-off publish job for a page.
|
|
103
|
+
* - Purges any previous jobs for this page
|
|
104
|
+
* - Uses a versioned jobId by default (or stable if useStableId=true)
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} params
|
|
107
|
+
* @param {string|number} params.page_id
|
|
108
|
+
* @param {string|number|Date} params.runAtUtc - ISO string, epoch ms, or Date (UTC)
|
|
109
|
+
* @param {object} [params.extra]
|
|
110
|
+
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
|
+
*/
|
|
112
|
+
async function addPagePublishJob({ page_id, runAtUtc, extra = {}, useStableId = false }) {
|
|
113
|
+
if (!page_id || !runAtUtc) {
|
|
114
|
+
throw new Error('addPagePublishJob: page_id and runAtUtc are required');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const runAt = normalizeToUtcDate(runAtUtc);
|
|
118
|
+
const runAtIso = runAt.toISOString();
|
|
119
|
+
|
|
120
|
+
// Drift guard to avoid immediate fire due to ms skew
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
let delayMs = runAt.getTime() - now;
|
|
123
|
+
if (delayMs < DRIFT_MS) delayMs = Math.max(0, DRIFT_MS);
|
|
124
|
+
|
|
125
|
+
// Purge any prior jobs for this page (across states)
|
|
126
|
+
await purgeExistingForPage(page_id);
|
|
127
|
+
|
|
128
|
+
// Choose jobId strategy
|
|
129
|
+
const jobId = useStableId ? stableJobId(page_id) : versionedJobId(page_id, runAtIso);
|
|
130
|
+
|
|
131
|
+
const job = await pagePublishQueue.add(
|
|
132
|
+
'page:publish',
|
|
133
|
+
{ page_id: String(page_id), runAtUtc: runAtIso, extra },
|
|
134
|
+
{ jobId, delay: delayMs }
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (DEBUG) {
|
|
138
|
+
console.info('[pagePublishQueue] upsert', {
|
|
139
|
+
page_id: String(page_id),
|
|
140
|
+
jobId,
|
|
141
|
+
runAtIso,
|
|
142
|
+
delayMs,
|
|
143
|
+
nowIso: new Date(now).toISOString(),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return job;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Cancel any scheduled publish for a given page (if present). */
|
|
151
|
+
async function cancelPagePublishJob(page_id) {
|
|
152
|
+
const removed = await purgeExistingForPage(page_id);
|
|
153
|
+
return removed > 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
pagePublishQueue,
|
|
158
|
+
addPagePublishJob,
|
|
159
|
+
cancelPagePublishJob,
|
|
160
|
+
// helpers for tooling/tests
|
|
161
|
+
stableJobId,
|
|
162
|
+
versionedJobId,
|
|
163
|
+
normalizeToUtcDate,
|
|
164
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// src/queue/predictionPublishQueue.js
|
|
2
|
+
// One-off *publish* scheduler for prediction items (BullMQ).
|
|
3
|
+
// Strong upsert: purge any prior job for the same prediction before adding a new delayed job.
|
|
4
|
+
|
|
5
|
+
const { Queue } = require('bullmq');
|
|
6
|
+
const redisClient = require('../redis/redisClient');
|
|
7
|
+
// const logger = require('../logger'); // optional
|
|
8
|
+
|
|
9
|
+
const DEBUG = String(process.env.DEBUG_LOGGER || '').trim() === '1';
|
|
10
|
+
const DRIFT_MS = Math.max(0, Number(process.env.SCHEDULE_DRIFT_MS || 250));
|
|
11
|
+
|
|
12
|
+
const predictionPublishQueue = new Queue('predictionPublishQueue', {
|
|
13
|
+
connection: redisClient,
|
|
14
|
+
defaultJobOptions: {
|
|
15
|
+
attempts: 5,
|
|
16
|
+
backoff: { type: 'exponential', delay: 2000 },
|
|
17
|
+
removeOnComplete: 500,
|
|
18
|
+
removeOnFail: 500,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ── ID helpers ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function stableJobId(prediction_id) {
|
|
25
|
+
return `prediction:publish:${prediction_id}`;
|
|
26
|
+
}
|
|
27
|
+
function versionedJobId(prediction_id, runAtIso) {
|
|
28
|
+
return `prediction:publish:${prediction_id}:${runAtIso}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Time helpers ───────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** Parse anything into a UTC Date. Strings without timezone are treated as UTC. */
|
|
34
|
+
function normalizeToUtcDate(input) {
|
|
35
|
+
if (input instanceof Date) return new Date(input.getTime());
|
|
36
|
+
if (typeof input === 'number') return new Date(input); // epoch ms
|
|
37
|
+
|
|
38
|
+
if (typeof input === 'string') {
|
|
39
|
+
// Has timezone (Z or ±hh:mm)
|
|
40
|
+
if (/[zZ]|[+\-]\d{2}:\d{2}$/.test(input)) {
|
|
41
|
+
const d = new Date(input);
|
|
42
|
+
if (Number.isNaN(d.getTime())) throw new Error(`Invalid ISO datetime: ${input}`);
|
|
43
|
+
return d;
|
|
44
|
+
}
|
|
45
|
+
// No timezone => treat as UTC
|
|
46
|
+
const s = input.trim().replace(' ', 'T');
|
|
47
|
+
const d = new Date(`${s}Z`);
|
|
48
|
+
if (Number.isNaN(d.getTime())) throw new Error(`Invalid datetime (no tz): ${input}`);
|
|
49
|
+
return d;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error(`Unsupported runAtUtc type: ${typeof input}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Purge helpers ─────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
async function findPendingPredictionJobs(prediction_id) {
|
|
58
|
+
const states = ['delayed', 'waiting', 'waiting-children', 'active'];
|
|
59
|
+
const jobs = await predictionPublishQueue.getJobs(states);
|
|
60
|
+
return jobs.filter(
|
|
61
|
+
(j) => j?.name === 'prediction:publish' && j?.data?.prediction_id === String(prediction_id)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function purgeExistingForPrediction(prediction_id) {
|
|
66
|
+
let removed = 0;
|
|
67
|
+
|
|
68
|
+
// Remove any pending/waiting/active jobs for this prediction
|
|
69
|
+
const pendings = await findPendingPredictionJobs(prediction_id);
|
|
70
|
+
for (const j of pendings) {
|
|
71
|
+
try {
|
|
72
|
+
await j.remove();
|
|
73
|
+
removed++;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (DEBUG) console.warn(`[predictionPublishQueue] failed to remove pending job ${j.id}: ${e.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Also remove a leftover stable job if present
|
|
80
|
+
const last = await predictionPublishQueue.getJob(stableJobId(prediction_id));
|
|
81
|
+
if (last) {
|
|
82
|
+
try {
|
|
83
|
+
await last.remove();
|
|
84
|
+
removed++;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (DEBUG) console.warn(`[predictionPublishQueue] failed to remove stable job ${last.id}: ${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (DEBUG) {
|
|
91
|
+
console.info('[predictionPublishQueue] purgeExistingForPrediction', {
|
|
92
|
+
prediction_id: String(prediction_id),
|
|
93
|
+
removed,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Upsert a one-off publish job for a prediction.
|
|
103
|
+
* - Purges any previous jobs for this prediction
|
|
104
|
+
* - Uses a versioned jobId by default (or stable if useStableId=true)
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} params
|
|
107
|
+
* @param {string|number} params.prediction_id
|
|
108
|
+
* @param {string|number|Date} params.runAtUtc - ISO string, epoch ms, or Date (UTC)
|
|
109
|
+
* @param {object} [params.extra]
|
|
110
|
+
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
|
+
*/
|
|
112
|
+
async function addPredictionPublishJob({ prediction_id, runAtUtc, extra = {}, useStableId = false }) {
|
|
113
|
+
if (!prediction_id || !runAtUtc) {
|
|
114
|
+
throw new Error('addPredictionPublishJob: prediction_id and runAtUtc are required');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const runAt = normalizeToUtcDate(runAtUtc);
|
|
118
|
+
const runAtIso = runAt.toISOString();
|
|
119
|
+
|
|
120
|
+
// Drift guard to avoid immediate fire due to ms skew
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
let delayMs = runAt.getTime() - now;
|
|
123
|
+
if (delayMs < DRIFT_MS) delayMs = Math.max(0, DRIFT_MS);
|
|
124
|
+
|
|
125
|
+
// Purge any prior jobs for this prediction (across states)
|
|
126
|
+
await purgeExistingForPrediction(prediction_id);
|
|
127
|
+
|
|
128
|
+
// Choose jobId strategy
|
|
129
|
+
const jobId = useStableId ? stableJobId(prediction_id) : versionedJobId(prediction_id, runAtIso);
|
|
130
|
+
|
|
131
|
+
const job = await predictionPublishQueue.add(
|
|
132
|
+
'prediction:publish',
|
|
133
|
+
{ prediction_id: String(prediction_id), runAtUtc: runAtIso, extra },
|
|
134
|
+
{ jobId, delay: delayMs }
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (DEBUG) {
|
|
138
|
+
console.info('[predictionPublishQueue] upsert', {
|
|
139
|
+
prediction_id: String(prediction_id),
|
|
140
|
+
jobId,
|
|
141
|
+
runAtIso,
|
|
142
|
+
delayMs,
|
|
143
|
+
nowIso: new Date(now).toISOString(),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return job;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Cancel any scheduled publish for a given prediction (if present). */
|
|
151
|
+
async function cancelPredictionPublishJob(prediction_id) {
|
|
152
|
+
const removed = await purgeExistingForPrediction(prediction_id);
|
|
153
|
+
return removed > 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
predictionPublishQueue,
|
|
158
|
+
addPredictionPublishJob,
|
|
159
|
+
cancelPredictionPublishJob,
|
|
160
|
+
// helpers for tooling/tests
|
|
161
|
+
stableJobId,
|
|
162
|
+
versionedJobId,
|
|
163
|
+
normalizeToUtcDate,
|
|
164
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// src/queue/productPublishQueue.js
|
|
2
|
+
// One-off *publish* scheduler for product items (BullMQ).
|
|
3
|
+
// Strong upsert: purge any prior job for the same product before adding a new delayed job.
|
|
4
|
+
|
|
5
|
+
const { Queue } = require('bullmq');
|
|
6
|
+
const redisClient = require('../redis/redisClient');
|
|
7
|
+
// const logger = require('../logger'); // uncomment if you have a shared logger
|
|
8
|
+
|
|
9
|
+
const DEBUG = String(process.env.DEBUG_LOGGER || '').trim() === '1';
|
|
10
|
+
const DRIFT_MS = Math.max(0, Number(process.env.SCHEDULE_DRIFT_MS || 250));
|
|
11
|
+
|
|
12
|
+
const productPublishQueue = new Queue('productPublishQueue', {
|
|
13
|
+
connection: redisClient,
|
|
14
|
+
defaultJobOptions: {
|
|
15
|
+
attempts: 5,
|
|
16
|
+
backoff: { type: 'exponential', delay: 2000 },
|
|
17
|
+
removeOnComplete: 500,
|
|
18
|
+
removeOnFail: 500,
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// ── ID helpers ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
function stableJobId(product_id) {
|
|
25
|
+
return `product:publish:${product_id}`;
|
|
26
|
+
}
|
|
27
|
+
function versionedJobId(product_id, runAtIso) {
|
|
28
|
+
return `product:publish:${product_id}:${runAtIso}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Time helpers ───────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/** Parse anything into a UTC Date. Strings without timezone are treated as UTC. */
|
|
34
|
+
function normalizeToUtcDate(input) {
|
|
35
|
+
if (input instanceof Date) return new Date(input.getTime());
|
|
36
|
+
if (typeof input === 'number') return new Date(input); // epoch ms
|
|
37
|
+
|
|
38
|
+
if (typeof input === 'string') {
|
|
39
|
+
// Has timezone (Z or ±hh:mm)
|
|
40
|
+
if (/[zZ]|[+\-]\d{2}:\d{2}$/.test(input)) {
|
|
41
|
+
const d = new Date(input);
|
|
42
|
+
if (Number.isNaN(d.getTime())) throw new Error(`Invalid ISO datetime: ${input}`);
|
|
43
|
+
return d;
|
|
44
|
+
}
|
|
45
|
+
// No timezone => treat as UTC
|
|
46
|
+
const s = input.trim().replace(' ', 'T');
|
|
47
|
+
const d = new Date(`${s}Z`);
|
|
48
|
+
if (Number.isNaN(d.getTime())) throw new Error(`Invalid datetime (no tz): ${input}`);
|
|
49
|
+
return d;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw new Error(`Unsupported runAtUtc type: ${typeof input}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Purge helpers ─────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
async function findPendingProductJobs(product_id) {
|
|
58
|
+
const states = ['delayed', 'waiting', 'waiting-children', 'active'];
|
|
59
|
+
const jobs = await productPublishQueue.getJobs(states);
|
|
60
|
+
return jobs.filter(
|
|
61
|
+
(j) => j?.name === 'product:publish' && j?.data?.product_id === String(product_id)
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function purgeExistingForProduct(product_id) {
|
|
66
|
+
let removed = 0;
|
|
67
|
+
|
|
68
|
+
// Remove any pending/waiting/active jobs for this product
|
|
69
|
+
const pendings = await findPendingProductJobs(product_id);
|
|
70
|
+
for (const j of pendings) {
|
|
71
|
+
try {
|
|
72
|
+
await j.remove();
|
|
73
|
+
removed++;
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (DEBUG) console.warn(`[productPublishQueue] failed to remove pending job ${j.id}: ${e.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Also remove a leftover stable job if present
|
|
80
|
+
const last = await productPublishQueue.getJob(stableJobId(product_id));
|
|
81
|
+
if (last) {
|
|
82
|
+
try {
|
|
83
|
+
await last.remove();
|
|
84
|
+
removed++;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (DEBUG) console.warn(`[productPublishQueue] failed to remove stable job ${last.id}: ${e.message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (DEBUG) {
|
|
91
|
+
console.info('[productPublishQueue] purgeExistingForProduct', {
|
|
92
|
+
product_id: String(product_id),
|
|
93
|
+
removed,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return removed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Upsert a one-off publish job for a product.
|
|
103
|
+
* - Purges any previous jobs for this product
|
|
104
|
+
* - Uses a versioned jobId by default (or stable if useStableId=true)
|
|
105
|
+
*
|
|
106
|
+
* @param {Object} params
|
|
107
|
+
* @param {string|number} params.product_id
|
|
108
|
+
* @param {string|number|Date} params.runAtUtc - ISO string, epoch ms, or Date (UTC)
|
|
109
|
+
* @param {object} [params.extra]
|
|
110
|
+
* @param {boolean} [params.useStableId=false] - if true, uses stable jobId
|
|
111
|
+
*/
|
|
112
|
+
async function addProductPublishJob({ product_id, runAtUtc, extra = {}, useStableId = false }) {
|
|
113
|
+
if (!product_id || !runAtUtc) {
|
|
114
|
+
throw new Error('addProductPublishJob: product_id and runAtUtc are required');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const runAt = normalizeToUtcDate(runAtUtc);
|
|
118
|
+
const runAtIso = runAt.toISOString();
|
|
119
|
+
|
|
120
|
+
// Drift guard to avoid immediate fire due to ms skew
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
let delayMs = runAt.getTime() - now;
|
|
123
|
+
if (delayMs < DRIFT_MS) delayMs = Math.max(0, DRIFT_MS);
|
|
124
|
+
|
|
125
|
+
// Purge any prior jobs for this product (across states)
|
|
126
|
+
await purgeExistingForProduct(product_id);
|
|
127
|
+
|
|
128
|
+
// Choose jobId strategy
|
|
129
|
+
const jobId = useStableId ? stableJobId(product_id) : versionedJobId(product_id, runAtIso);
|
|
130
|
+
|
|
131
|
+
const job = await productPublishQueue.add(
|
|
132
|
+
'product:publish',
|
|
133
|
+
{ product_id: String(product_id), runAtUtc: runAtIso, extra },
|
|
134
|
+
{ jobId, delay: delayMs }
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
if (DEBUG) {
|
|
138
|
+
console.info('[productPublishQueue] upsert', {
|
|
139
|
+
product_id: String(product_id),
|
|
140
|
+
jobId,
|
|
141
|
+
runAtIso,
|
|
142
|
+
delayMs,
|
|
143
|
+
nowIso: new Date(now).toISOString(),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return job;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Cancel any scheduled publish for a given product (if present). */
|
|
151
|
+
async function cancelProductPublishJob(product_id) {
|
|
152
|
+
const removed = await purgeExistingForProduct(product_id);
|
|
153
|
+
return removed > 0;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = {
|
|
157
|
+
productPublishQueue,
|
|
158
|
+
addProductPublishJob,
|
|
159
|
+
cancelProductPublishJob,
|
|
160
|
+
// helpers for tooling/tests
|
|
161
|
+
stableJobId,
|
|
162
|
+
versionedJobId,
|
|
163
|
+
normalizeToUtcDate,
|
|
164
|
+
};
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
const { compressionQueue } = require('../queue/compressionQueue');
|
|
2
2
|
const { newsTranslationQueue } = require('./newsTranslateQueue');
|
|
3
|
+
const {newsPublishQueue} = require('./newsPublishQueue');
|
|
4
|
+
const { articlePublishQueue } = require('./articlePublishQueue');
|
|
5
|
+
const {videoPublishQueue} = require('./videoPublishQueue');
|
|
6
|
+
const {imaagePublishQueue} = require('./imagePublishQueue');
|
|
7
|
+
const {pagePublishQueue} = require('./pagePublishQueue');
|
|
8
|
+
const {productPublishQueue} = require('./productPublishQueue');
|
|
9
|
+
const {quizPublishQueue} = require('./quizPublishQueue');
|
|
10
|
+
const {predictionPublishQueue} = require('./predictionPublishQueue');
|
|
3
11
|
// You can register more queues here like translationQueue, videoProcessingQueue, etc.
|
|
4
12
|
|
|
5
13
|
module.exports = [
|
|
@@ -10,6 +18,38 @@ module.exports = [
|
|
|
10
18
|
{
|
|
11
19
|
name: 'newsTranslationQueue',
|
|
12
20
|
instance: newsTranslationQueue
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'newsPublishQueue',
|
|
24
|
+
instance: newsPublishQueue
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'articlePublishQueue',
|
|
28
|
+
instance: articlePublishQueue
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'videoPublishQueue',
|
|
32
|
+
instance: videoPublishQueue
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'imaagePublishQueue',
|
|
36
|
+
instance: imaagePublishQueue
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'pagePublishQueue',
|
|
40
|
+
instance: pagePublishQueue
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'productPublishQueue',
|
|
44
|
+
instance: productPublishQueue
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'quizPublishQueue',
|
|
48
|
+
instance: quizPublishQueue
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'predictionPublishQueue',
|
|
52
|
+
instance: predictionPublishQueue
|
|
13
53
|
}
|
|
14
54
|
// Add more { name, instance } here
|
|
15
55
|
];
|