@yellowpanther/shared 1.3.0 → 1.3.2
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/index.js +1 -0
- package/src/queue/shopPublishQueue.js +187 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yellowpanther/shared",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -20,7 +20,6 @@
|
|
|
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",
|
|
24
23
|
"./queue/newsTranslateQueue": "./src/queue/newsTranslateQueue.js",
|
|
25
24
|
"./queue/newsPublishQueue": "./src/queue/newsPublishQueue.js",
|
|
26
25
|
"./queue/articlePublishQueue": "./src/queue/articlePublishQueue.js",
|
|
@@ -29,10 +28,12 @@
|
|
|
29
28
|
"./queue/imagePublishQueue": "./src/queue/imagePublishQueue.js",
|
|
30
29
|
"./queue/imageAlbumPublishQueue": "./src/queue/imageAlbumPublishQueue.js",
|
|
31
30
|
"./queue/pagePublishQueue": "./src/queue/pagePublishQueue.js",
|
|
31
|
+
"./queue/shopPublishQueue": "./src/queue/shopPublishQueue.js",
|
|
32
32
|
"./queue/productPublishQueue": "./src/queue/productPublishQueue.js",
|
|
33
33
|
"./queue/quizPublishQueue": "./src/queue/quizPublishQueue.js",
|
|
34
34
|
"./queue/predictPublishQueue": "./src/queue/predictPublishQueue.js",
|
|
35
35
|
"./queue/queueRegistry": "./src/queue/queueRegistry.js",
|
|
36
|
+
"./queue/addUserPointsQueue": "./src/queue/addUserPointsQueue.js",
|
|
36
37
|
"./lang/translationHelper": "./src/lang/translationHelper.js",
|
|
37
38
|
"./*": "./src/*"
|
|
38
39
|
},
|
package/src/index.js
CHANGED
|
@@ -30,5 +30,6 @@ module.exports = {
|
|
|
30
30
|
addImageAlbumPublishJob: require("./queue/imageAlbumPublishQueue")
|
|
31
31
|
.addImageAlbumPublishJob,
|
|
32
32
|
addEventPublishJob: require("./queue/eventPublishQueue").addEventPublishJob,
|
|
33
|
+
addShopPublishJob: require("./queue/shopPublishQueue").addShopPublishJob,
|
|
33
34
|
addUserPointsJob: require("./queue/addUserPointsQueue").addUserPointsJob,
|
|
34
35
|
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// src/queue/shopPublishQueue.js
|
|
2
|
+
// One-off *publish* scheduler for shop items (BullMQ).
|
|
3
|
+
// Strong upsert: purge any prior job for the same shop 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 shopPublishQueue = new Queue("shopPublishQueue", {
|
|
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(shop_id) {
|
|
31
|
+
return sanitizeId(`shop:publish:${shop_id}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Versioned id (keeps history distinct) */
|
|
35
|
+
function versionedJobId(shop_id, runAtIso) {
|
|
36
|
+
return sanitizeId(`shop:publish:${shop_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 findPendingShopJobs(shop_id) {
|
|
68
|
+
const states = ["delayed", "waiting", "waiting-children", "active"];
|
|
69
|
+
const jobs = await shopPublishQueue.getJobs(states);
|
|
70
|
+
return jobs.filter(
|
|
71
|
+
(j) => j?.name === "shop:publish" && j?.data?.shop_id === String(shop_id),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function purgeExistingForShop(shop_id) {
|
|
76
|
+
let removed = 0;
|
|
77
|
+
|
|
78
|
+
// Remove any pending/waiting/active jobs for this shop
|
|
79
|
+
const pendings = await findPendingShopJobs(shop_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
|
+
`[shopPublishQueue] 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 shopPublishQueue.getJob(stableJobId(shop_id));
|
|
94
|
+
if (last) {
|
|
95
|
+
try {
|
|
96
|
+
await last.remove();
|
|
97
|
+
removed++;
|
|
98
|
+
} catch (e) {
|
|
99
|
+
if (DEBUG)
|
|
100
|
+
console.warn(
|
|
101
|
+
`[shopPublishQueue] failed to remove stable job ${last.id}: ${e.message}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (DEBUG) {
|
|
107
|
+
console.info("[shopPublishQueue] purgeExistingForShop", {
|
|
108
|
+
shop_id: String(shop_id),
|
|
109
|
+
removed,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return removed;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ── Public API ───────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Upsert a one-off publish job for a shop item.
|
|
119
|
+
* - Purges any previous jobs for this shop
|
|
120
|
+
* - Uses a versioned jobId by default (or stable if useStableId=true)
|
|
121
|
+
*
|
|
122
|
+
* @param {Object} params
|
|
123
|
+
* @param {string|number} params.shop_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 addShopPublishJob({
|
|
129
|
+
shop_id,
|
|
130
|
+
runAtUtc,
|
|
131
|
+
extra = {},
|
|
132
|
+
useStableId = false,
|
|
133
|
+
}) {
|
|
134
|
+
if (!shop_id || !runAtUtc) {
|
|
135
|
+
throw new Error("addShopPublishJob: shop_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 shop (across states)
|
|
147
|
+
await purgeExistingForShop(shop_id);
|
|
148
|
+
|
|
149
|
+
// Choose jobId strategy
|
|
150
|
+
const jobId = useStableId
|
|
151
|
+
? stableJobId(shop_id)
|
|
152
|
+
: versionedJobId(shop_id, runAtIso);
|
|
153
|
+
|
|
154
|
+
const job = await shopPublishQueue.add(
|
|
155
|
+
"shop:publish",
|
|
156
|
+
{ shop_id: String(shop_id), runAtUtc: runAtIso, extra },
|
|
157
|
+
{ jobId, delay: delayMs },
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
if (DEBUG) {
|
|
161
|
+
console.info("[shopPublishQueue] upsert", {
|
|
162
|
+
shop_id: String(shop_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 shop (if present). */
|
|
174
|
+
async function cancelShopPublishJob(shop_id) {
|
|
175
|
+
const removed = await purgeExistingForShop(shop_id);
|
|
176
|
+
return removed > 0;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
shopPublishQueue,
|
|
181
|
+
addShopPublishJob,
|
|
182
|
+
cancelShopPublishJob,
|
|
183
|
+
// helpers (useful for tooling/tests)
|
|
184
|
+
stableJobId,
|
|
185
|
+
versionedJobId,
|
|
186
|
+
normalizeToUtcDate,
|
|
187
|
+
};
|