groupmq-plus 1.1.1 → 1.1.3

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/dist/index.cjs CHANGED
@@ -240,6 +240,7 @@ var Job = class Job {
240
240
  this.timestamp = args.timestamp;
241
241
  this.orderMs = args.orderMs;
242
242
  this.status = args.status ?? "unknown";
243
+ this.parentId = args.parentId;
243
244
  }
244
245
  async getState() {
245
246
  return this.status ?? "unknown";
@@ -288,6 +289,39 @@ var Job = class Job {
288
289
  async waitUntilFinished(timeoutMs = 0) {
289
290
  return this.queue.waitUntilFinished(this.id, timeoutMs);
290
291
  }
292
+ /**
293
+ * Get all child jobs of this job (if it's a parent in a flow).
294
+ * @returns Array of child Job instances
295
+ */
296
+ async getChildren() {
297
+ return this.queue.getFlowChildren(this.id);
298
+ }
299
+ /**
300
+ * Get the return values of all child jobs in a flow.
301
+ * @returns Object mapping child job IDs to their return values
302
+ */
303
+ async getChildrenValues() {
304
+ return this.queue.getFlowResults(this.id);
305
+ }
306
+ /**
307
+ * Get the number of remaining child jobs that haven't completed yet.
308
+ * @returns Number of remaining dependencies, or null if not a parent job
309
+ */
310
+ async getDependenciesCount() {
311
+ return this.queue.getFlowDependencies(this.id);
312
+ }
313
+ /**
314
+ * Get the parent job of this job (if it's a child in a flow).
315
+ * @returns Parent Job instance, or undefined if no parent or parent was deleted
316
+ */
317
+ async getParent() {
318
+ if (!this.parentId) return void 0;
319
+ try {
320
+ return await this.queue.getJob(this.parentId);
321
+ } catch (_e) {
322
+ return;
323
+ }
324
+ }
291
325
  static fromReserved(queue, reserved, meta) {
292
326
  return new Job({
293
327
  queue,
@@ -327,6 +361,7 @@ var Job = class Job {
327
361
  const failedReason = (raw.failedReason ?? raw.lastErrorMessage) || void 0;
328
362
  const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || void 0;
329
363
  const returnvalue = raw.returnvalue ? safeJsonParse$1(raw.returnvalue) : void 0;
364
+ const parentId = raw.parentId || void 0;
330
365
  return new Job({
331
366
  queue,
332
367
  id,
@@ -345,7 +380,8 @@ var Job = class Job {
345
380
  returnvalue,
346
381
  timestamp: timestampMs || Date.now(),
347
382
  orderMs,
348
- status: knownStatus ?? coerceStatus(raw.status)
383
+ status: knownStatus ?? coerceStatus(raw.status),
384
+ parentId
349
385
  });
350
386
  }
351
387
  static async fromStore(queue, id) {
@@ -364,6 +400,7 @@ var Job = class Job {
364
400
  const failedReason = (raw.failedReason ?? raw.lastErrorMessage) || void 0;
365
401
  const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || void 0;
366
402
  const returnvalue = raw.returnvalue ? safeJsonParse$1(raw.returnvalue) : void 0;
403
+ const parentId = raw.parentId || void 0;
367
404
  const [inProcessing, inDelayed] = await Promise.all([queue.redis.zscore(`${queue.namespace}:processing`, id), queue.redis.zscore(`${queue.namespace}:delayed`, id)]);
368
405
  let status = raw.status;
369
406
  if (inProcessing !== null) status = "active";
@@ -389,7 +426,8 @@ var Job = class Job {
389
426
  returnvalue,
390
427
  timestamp: timestampMs || Date.now(),
391
428
  orderMs,
392
- status: coerceStatus(status)
429
+ status: coerceStatus(status),
430
+ parentId
393
431
  });
394
432
  }
395
433
  };
@@ -637,6 +675,38 @@ var Queue = class {
637
675
  }
638
676
  return parsed;
639
677
  }
678
+ /**
679
+ * Gets all child job IDs for a parent job in a flow.
680
+ * @param parentId The ID of the parent job
681
+ * @returns An array of child job IDs
682
+ */
683
+ async getFlowChildrenIds(parentId) {
684
+ return this.r.smembers(`${this.ns}:flow:children:${parentId}`);
685
+ }
686
+ /**
687
+ * Gets all child jobs for a parent job in a flow.
688
+ * @param parentId The ID of the parent job
689
+ * @returns An array of Job instances for all children
690
+ */
691
+ async getFlowChildren(parentId) {
692
+ const ids = await this.getFlowChildrenIds(parentId);
693
+ if (ids.length === 0) return [];
694
+ const pipe = this.r.multi();
695
+ for (const id of ids) pipe.hgetall(`${this.ns}:job:${id}`);
696
+ const rows = await pipe.exec();
697
+ const jobs = [];
698
+ for (let i = 0; i < ids.length; i++) {
699
+ const id = ids[i];
700
+ const raw = rows?.[i]?.[1] || {};
701
+ if (!raw || Object.keys(raw).length === 0) {
702
+ this.logger.warn(`Skipping child job ${id} - not found (likely cleaned up)`);
703
+ continue;
704
+ }
705
+ const job = Job.fromRawHash(this, id, raw);
706
+ jobs.push(job);
707
+ }
708
+ return jobs;
709
+ }
640
710
  async addSingle(opts) {
641
711
  const now = Date.now();
642
712
  let delayUntil = 0;
@@ -2520,7 +2590,10 @@ var _Worker = class extends TypedEventEmitter {
2520
2590
  const oldest = Array.from(this.jobsInProgress)[0];
2521
2591
  const now = Date.now();
2522
2592
  return {
2523
- job: oldest.job,
2593
+ job: Job.fromReserved(this.q, oldest.job, {
2594
+ processedOn: oldest.ts,
2595
+ status: "active"
2596
+ }),
2524
2597
  processingTimeMs: now - oldest.ts
2525
2598
  };
2526
2599
  }
@@ -2530,7 +2603,10 @@ var _Worker = class extends TypedEventEmitter {
2530
2603
  getCurrentJobs() {
2531
2604
  const now = Date.now();
2532
2605
  return Array.from(this.jobsInProgress).map((item) => ({
2533
- job: item.job,
2606
+ job: Job.fromReserved(this.q, item.job, {
2607
+ processedOn: item.ts,
2608
+ status: "active"
2609
+ }),
2534
2610
  processingTimeMs: now - item.ts
2535
2611
  }));
2536
2612
  }
@@ -2560,7 +2636,7 @@ var _Worker = class extends TypedEventEmitter {
2560
2636
  } catch (e) {
2561
2637
  const isConnErr = this.q.isConnectionError(e);
2562
2638
  if (!isConnErr || !this.stopping) this.logger.error(`Heartbeat error for job ${job.id}:`, e instanceof Error ? e.message : String(e));
2563
- this.onError?.(e, job);
2639
+ this.onError?.(e, Job.fromReserved(this.q, job, { status: "active" }));
2564
2640
  if (!isConnErr || !this.stopping) this.emit("error", e instanceof Error ? e : new Error(String(e)));
2565
2641
  }
2566
2642
  }, minInterval);
@@ -2571,7 +2647,11 @@ var _Worker = class extends TypedEventEmitter {
2571
2647
  heartbeatDelayTimer = setTimeout(() => {
2572
2648
  startHeartbeat();
2573
2649
  }, heartbeatThreshold);
2574
- const handlerResult = await this.handler(job);
2650
+ const jobInstance = Job.fromReserved(this.q, job, {
2651
+ processedOn: jobStartWallTime,
2652
+ status: "active"
2653
+ });
2654
+ const handlerResult = await this.handler(jobInstance);
2575
2655
  if (heartbeatDelayTimer) clearTimeout(heartbeatDelayTimer);
2576
2656
  if (hbTimer) clearInterval(hbTimer);
2577
2657
  const finishedAtWall = Date.now();
@@ -2595,7 +2675,11 @@ var _Worker = class extends TypedEventEmitter {
2595
2675
  * Handle job failure: emit events, retry or dead-letter
2596
2676
  */
2597
2677
  async handleJobFailure(err, job, jobStartWallTime) {
2598
- this.onError?.(err, job);
2678
+ const jobInstance = Job.fromReserved(this.q, job, {
2679
+ processedOn: jobStartWallTime,
2680
+ status: "active"
2681
+ });
2682
+ this.onError?.(err, jobInstance);
2599
2683
  this.blockingStats.consecutiveEmptyReserves = 0;
2600
2684
  this.emptyReserveBackoffMs = 0;
2601
2685
  try {
@@ -2681,9 +2765,122 @@ function sleep(ms) {
2681
2765
  return new Promise((r) => setTimeout(r, ms));
2682
2766
  }
2683
2767
 
2768
+ //#endregion
2769
+ //#region src/strategies/priority-strategy.ts
2770
+ var PriorityStrategy = class {
2771
+ constructor(options = {}) {
2772
+ this.cache = /* @__PURE__ */ new Map();
2773
+ this.overrides = /* @__PURE__ */ new Map();
2774
+ this.algorithmConfig = options.algorithm ?? { type: "weighted-random" };
2775
+ this.defaultPriority = options.defaultPriority ?? 1;
2776
+ this.cacheTtlMs = options.cacheTtlMs ?? 5e3;
2777
+ this.onGetPriority = options.onGetPriority;
2778
+ }
2779
+ /**
2780
+ * 手动覆盖某个组的优先级(优先级高于 Redis 配置和 getPriority)
2781
+ * 主要用于测试或临时调整
2782
+ */
2783
+ setPriority(groupId, priority) {
2784
+ this.overrides.set(groupId, priority);
2785
+ }
2786
+ /**
2787
+ * 清除手动覆盖,恢复使用 Redis 配置或 getPriority
2788
+ */
2789
+ clearPriority(groupId) {
2790
+ this.overrides.delete(groupId);
2791
+ }
2792
+ /**
2793
+ * 获取组的基础优先级
2794
+ * 优先级来源顺序:overrides > cache > onGetPriority(config) 或 config.priority
2795
+ */
2796
+ async resolvePriority(queue, groupId) {
2797
+ const override = this.overrides.get(groupId);
2798
+ if (override !== void 0) return override;
2799
+ const now = Date.now();
2800
+ const cached = this.cache.get(groupId);
2801
+ if (cached && cached.expiresAt > now) return cached.priority;
2802
+ const config = await queue.getGroupConfig(groupId);
2803
+ let priority;
2804
+ if (this.onGetPriority) priority = await this.onGetPriority(groupId, config);
2805
+ else priority = config.priority !== 1 ? config.priority : this.defaultPriority;
2806
+ if (this.cacheTtlMs > 0) this.cache.set(groupId, {
2807
+ priority,
2808
+ expiresAt: now + this.cacheTtlMs
2809
+ });
2810
+ return priority;
2811
+ }
2812
+ /**
2813
+ * 严格优先级算法:总是返回优先级最高的组
2814
+ */
2815
+ selectByStrict(groups) {
2816
+ groups.sort((a, b) => b.priority - a.priority);
2817
+ return groups[0].groupId;
2818
+ }
2819
+ /**
2820
+ * 加权随机算法:根据优先级计算概率选择
2821
+ * 使用 minWeightRatio 确保低优先级组也有机会
2822
+ */
2823
+ selectByWeightedRandom(groups) {
2824
+ if (groups.length === 1) return groups[0].groupId;
2825
+ const minWeightRatio = this.algorithmConfig.minWeightRatio ?? .1;
2826
+ const minWeight = Math.max(...groups.map((g) => g.priority)) * minWeightRatio;
2827
+ const weights = groups.map((g) => ({
2828
+ groupId: g.groupId,
2829
+ weight: Math.max(g.priority, minWeight)
2830
+ }));
2831
+ const totalWeight = weights.reduce((sum, w) => sum + w.weight, 0);
2832
+ let random = Math.random() * totalWeight;
2833
+ for (const w of weights) {
2834
+ random -= w.weight;
2835
+ if (random <= 0) return w.groupId;
2836
+ }
2837
+ return groups[0].groupId;
2838
+ }
2839
+ /**
2840
+ * 时间衰减算法:等待时间越长,优先级加成越高
2841
+ */
2842
+ selectByAging(groups) {
2843
+ const now = Date.now();
2844
+ const intervalMs = this.algorithmConfig.intervalMs ?? 6e4;
2845
+ const adjustedGroups = groups.map((g) => {
2846
+ let ageBonus = 0;
2847
+ if (g.oldestTimestamp) {
2848
+ const waitTime = now - g.oldestTimestamp;
2849
+ ageBonus = Math.floor(waitTime / intervalMs);
2850
+ }
2851
+ return {
2852
+ groupId: g.groupId,
2853
+ adjustedPriority: g.priority + ageBonus
2854
+ };
2855
+ });
2856
+ adjustedGroups.sort((a, b) => b.adjustedPriority - a.adjustedPriority);
2857
+ return adjustedGroups[0].groupId;
2858
+ }
2859
+ async getNextGroup(queue) {
2860
+ const readyGroups = await queue.getReadyGroups(0, 100);
2861
+ if (readyGroups.length === 0) return null;
2862
+ const groupInfos = await Promise.all(readyGroups.map(async (groupId) => {
2863
+ const priority = await this.resolvePriority(queue, groupId);
2864
+ const info = {
2865
+ groupId,
2866
+ priority
2867
+ };
2868
+ if (this.algorithmConfig.type === "aging") info.oldestTimestamp = await queue.getGroupOldestTimestamp(groupId);
2869
+ return info;
2870
+ }));
2871
+ switch (this.algorithmConfig.type) {
2872
+ case "strict": return this.selectByStrict(groupInfos);
2873
+ case "aging": return this.selectByAging(groupInfos);
2874
+ case "weighted-random":
2875
+ default: return this.selectByWeightedRandom(groupInfos);
2876
+ }
2877
+ }
2878
+ };
2879
+
2684
2880
  //#endregion
2685
2881
  exports.BullBoardGroupMQAdapter = BullBoardGroupMQAdapter;
2686
2882
  exports.Job = Job;
2883
+ exports.PriorityStrategy = PriorityStrategy;
2687
2884
  exports.Queue = Queue;
2688
2885
  exports.UnrecoverableError = UnrecoverableError;
2689
2886
  exports.Worker = Worker;