@yellowpanther/shared 1.2.7 → 1.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yellowpanther/shared",
3
- "version": "1.2.7",
3
+ "version": "1.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -20,9 +20,11 @@
20
20
  "./redis/redisClient": "./src/redis/redisClient.js",
21
21
  "./queue/compressionQueue": "./src/queue/compressionQueue.js",
22
22
  "./queue/newsTranslationQueue": "./src/queue/newsTranslationQueue.js",
23
+ "./queue/addUserPointsQueue": "./src/queue/addUserPointsQueue.js",
23
24
  "./queue/newsTranslateQueue": "./src/queue/newsTranslateQueue.js",
24
25
  "./queue/newsPublishQueue": "./src/queue/newsPublishQueue.js",
25
26
  "./queue/articlePublishQueue": "./src/queue/articlePublishQueue.js",
27
+ "./queue/eventPublishQueue": "./src/queue/eventPublishQueue.js",
26
28
  "./queue/videoPublishQueue": "./src/queue/videoPublishQueue.js",
27
29
  "./queue/imagePublishQueue": "./src/queue/imagePublishQueue.js",
28
30
  "./queue/imageAlbumPublishQueue": "./src/queue/imageAlbumPublishQueue.js",
package/src/index.js CHANGED
@@ -1,25 +1,34 @@
1
+ const { addEventPublishJob } = require("./queue/eventPublishQueue");
2
+
1
3
  module.exports = {
2
4
  // ✅ Redis
3
- redisClient: require('./redis/redisClient'),
5
+ redisClient: require("./redis/redisClient"),
4
6
 
5
7
  // ✅ Language tranlation Helper
6
- translationHelper: require('./lang/translationHelper'),
8
+ translationHelper: require("./lang/translationHelper"),
7
9
 
8
10
  // ✅ Config
9
- config: require('./config'),
11
+ config: require("./config"),
10
12
 
11
13
  // ✅ Queues
12
- compressionQueue: require('./queue/compressionQueue'),
13
- addCompressionJob: require('./queue/compressionQueue').addCompressionJob,
14
- newsTranslateQueue: require('./queue/newsTranslateQueue'),
15
- addNewsTranslationJob: require('./queue/newsTranslateQueue').addNewsTranslationJob,
16
- addNewsPublishJob: require('./queue/newsPublishQueue').addNewsPublishJob,
17
- addArticlePublishJob: require('./queue/articlePublishQueue').addArticlePublishJob,
18
- addVideoPublishJob: require('./queue/videoPublishQueue').addVideoPublishJob,
19
- addImagePublishJob: require('./queue/imagePublishQueue').addImagePublishJob,
20
- addPagePublishJob: require('./queue/pagePublishQueue').addPagePublishJob,
21
- addProductPublishJob: require('./queue/productPublishQueue').addProductPublishJob,
22
- addQuizPublishJob: require('./queue/quizPublishQueue').addQuizPublishJob,
23
- addPredictionPublishJob: require('./queue/predictionPublishQueue').addPredictionPublishJob,
24
- addImageAlbumPublishJob: require('./queue/imageAlbumPublishQueue').addImageAlbumPublishJob,
25
- };
14
+ compressionQueue: require("./queue/compressionQueue"),
15
+ addCompressionJob: require("./queue/compressionQueue").addCompressionJob,
16
+ newsTranslateQueue: require("./queue/newsTranslateQueue"),
17
+ addNewsTranslationJob: require("./queue/newsTranslateQueue")
18
+ .addNewsTranslationJob,
19
+ addNewsPublishJob: require("./queue/newsPublishQueue").addNewsPublishJob,
20
+ addArticlePublishJob: require("./queue/articlePublishQueue")
21
+ .addArticlePublishJob,
22
+ addVideoPublishJob: require("./queue/videoPublishQueue").addVideoPublishJob,
23
+ addImagePublishJob: require("./queue/imagePublishQueue").addImagePublishJob,
24
+ addPagePublishJob: require("./queue/pagePublishQueue").addPagePublishJob,
25
+ addProductPublishJob: require("./queue/productPublishQueue")
26
+ .addProductPublishJob,
27
+ addQuizPublishJob: require("./queue/quizPublishQueue").addQuizPublishJob,
28
+ addPredictionPublishJob: require("./queue/predictionPublishQueue")
29
+ .addPredictionPublishJob,
30
+ addImageAlbumPublishJob: require("./queue/imageAlbumPublishQueue")
31
+ .addImageAlbumPublishJob,
32
+ addEventPublishJob: require("./queue/eventPublishQueue").addEventPublishJob,
33
+ addUserPointsJob: require("./queue/addUserPointsQueue").addUserPointsJob,
34
+ };
@@ -0,0 +1,49 @@
1
+
2
+ const { Queue } = require("bullmq");
3
+ const redisClient = require("../redis/redisClient");
4
+
5
+ const DEBUG = String(process.env.DEBUG_LOGGER || "").trim() === "1";
6
+
7
+ const addUserPointsQueue = new Queue("addUserPointsQueue", {
8
+ connection: redisClient,
9
+ defaultJobOptions: {
10
+ attempts: 5,
11
+ backoff: { type: "exponential", delay: 2000 },
12
+ removeOnComplete: 500,
13
+ removeOnFail: 500,
14
+ },
15
+ });
16
+
17
+ function sanitizeId(str) {
18
+ return str.replace(/[:.]/g, "-");
19
+ }
20
+
21
+ function stableJobId(option_id) {
22
+ return sanitizeId(`addUserPointsQueue:${option_id}`);
23
+ }
24
+
25
+ function versionedJobId(option_id) {
26
+ return sanitizeId(`addUserPoints:${option_id}`);
27
+ }
28
+
29
+ async function addUserPointsJob(correctOption, useStableId = false){
30
+ // const jobId = useStableId
31
+ // ? stableJobId(correctOption.id)
32
+ // : versionedJobId(correctOption.id);
33
+
34
+ const job = await addUserPointsQueue.add(
35
+ "addUserPointsQueue",
36
+ { option_id: correctOption.id },
37
+ // { jobId }
38
+ );
39
+
40
+ if (DEBUG) {
41
+ console.info("[addUserPointsQueue] upsert", {
42
+ option_id: correctOption.id,
43
+ // jobId,
44
+ });
45
+ }
46
+ return job;
47
+ }
48
+
49
+ module.exports = {addUserPointsJob}
@@ -0,0 +1,187 @@
1
+ // src/queue/eventPublishQueue.js
2
+ // One-off *publish* scheduler for event items (BullMQ).
3
+ // Strong upsert: purge any prior job for the same event 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 eventPublishQueue = new Queue("eventPublishQueue", {
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
+ /** Replace invalid chars for BullMQ IDs */
25
+ function sanitizeId(str) {
26
+ return str.replace(/[:.]/g, "-");
27
+ }
28
+
29
+ /** Stable id (latest schedule) */
30
+ function stableJobId(event_id) {
31
+ return sanitizeId(`event:publish:${event_id}`);
32
+ }
33
+
34
+ /** Versioned id (keeps history distinct) */
35
+ function versionedJobId(event_id, runAtIso) {
36
+ return sanitizeId(`event:publish:${event_id}:${runAtIso}`);
37
+ }
38
+
39
+ // --- Time helpers ------------------------------------------------------------
40
+
41
+ /** Parse anything into a UTC Date. Strings without timezone are treated as UTC. */
42
+ function normalizeToUtcDate(input) {
43
+ if (input instanceof Date) return new Date(input.getTime());
44
+ if (typeof input === "number") return new Date(input); // epoch ms
45
+
46
+ if (typeof input === "string") {
47
+ // Has timezone (Z or ±hh:mm)
48
+ if (/[zZ]|[+\-]\d{2}:\d{2}$/.test(input)) {
49
+ const d = new Date(input);
50
+ if (Number.isNaN(d.getTime()))
51
+ throw new Error(`Invalid ISO datetime: ${input}`);
52
+ return d;
53
+ }
54
+ // No timezone => treat as UTC
55
+ const s = input.trim().replace(" ", "T");
56
+ const d = new Date(`${s}Z`);
57
+ if (Number.isNaN(d.getTime()))
58
+ throw new Error(`Invalid datetime (no tz): ${input}`);
59
+ return d;
60
+ }
61
+
62
+ throw new Error(`Unsupported runAtUtc type: ${typeof input}`);
63
+ }
64
+
65
+ // --- Purge helpers -----------------------------------------------------------
66
+
67
+ async function findPendingEventJobs(event_id) {
68
+ const states = ["delayed", "waiting", "waiting-children", "active"];
69
+ const jobs = await eventPublishQueue.getJobs(states);
70
+ return jobs.filter(
71
+ (j) => j?.name === "event:publish" && j?.data?.event_id === String(event_id)
72
+ );
73
+ }
74
+
75
+ async function purgeExistingForEvent(event_id) {
76
+ let removed = 0;
77
+
78
+ // Remove any pending/waiting/active jobs for this event
79
+ const pendings = await findPendingEventJobs(event_id);
80
+ for (const j of pendings) {
81
+ try {
82
+ await j.remove();
83
+ removed++;
84
+ } catch (e) {
85
+ if (DEBUG)
86
+ console.warn(
87
+ `[eventPublishQueue] failed to remove pending job ${j.id}: ${e.message}`
88
+ );
89
+ }
90
+ }
91
+
92
+ // Also remove a leftover stable job if present
93
+ const last = await eventPublishQueue.getJob(stableJobId(event_id));
94
+ if (last) {
95
+ try {
96
+ await last.remove();
97
+ removed++;
98
+ } catch (e) {
99
+ if (DEBUG)
100
+ console.warn(
101
+ `[eventPublishQueue] failed to remove stable job ${last.id}: ${e.message}`
102
+ );
103
+ }
104
+ }
105
+
106
+ if (DEBUG) {
107
+ console.info("[eventPublishQueue] purgeExistingForEvent", {
108
+ event_id: String(event_id),
109
+ removed,
110
+ });
111
+ }
112
+ return removed;
113
+ }
114
+
115
+ // --- Public API --------------------------------------------------------------
116
+
117
+ /**
118
+ * Upsert a one-off publish job for an event.
119
+ * - Purges any previous jobs for this event
120
+ * - Uses a versioned jobId by default (or stable if useStableId=true)
121
+ *
122
+ * @param {Object} params
123
+ * @param {string|number} params.event_id
124
+ * @param {string|number|Date} params.runAtUtc - ISO string, epoch ms, or Date (UTC)
125
+ * @param {object} [params.extra]
126
+ * @param {boolean} [params.useStableId=false] - if true, uses stable jobId
127
+ */
128
+ async function addEventPublishJob({
129
+ event_id,
130
+ runAtUtc,
131
+ extra = {},
132
+ useStableId = false,
133
+ }) {
134
+ if (!event_id || !runAtUtc) {
135
+ throw new Error("addEventPublishJob: event_id and runAtUtc are required");
136
+ }
137
+
138
+ const runAt = normalizeToUtcDate(runAtUtc);
139
+ const runAtIso = runAt.toISOString();
140
+
141
+ // drift guard to avoid immediate fire due to ms skew
142
+ const now = Date.now();
143
+ let delayMs = runAt.getTime() - now;
144
+ if (delayMs < DRIFT_MS) delayMs = Math.max(0, DRIFT_MS);
145
+
146
+ // Purge any prior jobs for this event (across states)
147
+ await purgeExistingForEvent(event_id);
148
+
149
+ // Choose jobId strategy
150
+ const jobId = useStableId
151
+ ? stableJobId(event_id)
152
+ : versionedJobId(event_id, runAtIso);
153
+
154
+ const job = await eventPublishQueue.add(
155
+ "event:publish",
156
+ { event_id: String(event_id), runAtUtc: runAtIso, extra },
157
+ { jobId, delay: delayMs }
158
+ );
159
+
160
+ if (DEBUG) {
161
+ console.info("[eventPublishQueue] upsert", {
162
+ event_id: String(event_id),
163
+ jobId,
164
+ runAtIso,
165
+ delayMs,
166
+ nowIso: new Date(now).toISOString(),
167
+ });
168
+ }
169
+
170
+ return job;
171
+ }
172
+
173
+ /** Cancel any scheduled publish for a given event (if present). */
174
+ async function cancelEventPublishJob(event_id) {
175
+ const removed = await purgeExistingForEvent(event_id);
176
+ return removed > 0;
177
+ }
178
+
179
+ module.exports = {
180
+ eventPublishQueue,
181
+ addEventPublishJob,
182
+ cancelEventPublishJob,
183
+ // exported helpers for tooling/tests
184
+ stableJobId,
185
+ versionedJobId,
186
+ normalizeToUtcDate,
187
+ };