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.js CHANGED
@@ -235,6 +235,7 @@ var Job = class Job {
235
235
  this.timestamp = args.timestamp;
236
236
  this.orderMs = args.orderMs;
237
237
  this.status = args.status ?? "unknown";
238
+ this.parentId = args.parentId;
238
239
  }
239
240
  async getState() {
240
241
  return this.status ?? "unknown";
@@ -283,6 +284,39 @@ var Job = class Job {
283
284
  async waitUntilFinished(timeoutMs = 0) {
284
285
  return this.queue.waitUntilFinished(this.id, timeoutMs);
285
286
  }
287
+ /**
288
+ * Get all child jobs of this job (if it's a parent in a flow).
289
+ * @returns Array of child Job instances
290
+ */
291
+ async getChildren() {
292
+ return this.queue.getFlowChildren(this.id);
293
+ }
294
+ /**
295
+ * Get the return values of all child jobs in a flow.
296
+ * @returns Object mapping child job IDs to their return values
297
+ */
298
+ async getChildrenValues() {
299
+ return this.queue.getFlowResults(this.id);
300
+ }
301
+ /**
302
+ * Get the number of remaining child jobs that haven't completed yet.
303
+ * @returns Number of remaining dependencies, or null if not a parent job
304
+ */
305
+ async getDependenciesCount() {
306
+ return this.queue.getFlowDependencies(this.id);
307
+ }
308
+ /**
309
+ * Get the parent job of this job (if it's a child in a flow).
310
+ * @returns Parent Job instance, or undefined if no parent or parent was deleted
311
+ */
312
+ async getParent() {
313
+ if (!this.parentId) return void 0;
314
+ try {
315
+ return await this.queue.getJob(this.parentId);
316
+ } catch (_e) {
317
+ return;
318
+ }
319
+ }
286
320
  static fromReserved(queue, reserved, meta) {
287
321
  return new Job({
288
322
  queue,
@@ -322,6 +356,7 @@ var Job = class Job {
322
356
  const failedReason = (raw.failedReason ?? raw.lastErrorMessage) || void 0;
323
357
  const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || void 0;
324
358
  const returnvalue = raw.returnvalue ? safeJsonParse$1(raw.returnvalue) : void 0;
359
+ const parentId = raw.parentId || void 0;
325
360
  return new Job({
326
361
  queue,
327
362
  id,
@@ -340,7 +375,8 @@ var Job = class Job {
340
375
  returnvalue,
341
376
  timestamp: timestampMs || Date.now(),
342
377
  orderMs,
343
- status: knownStatus ?? coerceStatus(raw.status)
378
+ status: knownStatus ?? coerceStatus(raw.status),
379
+ parentId
344
380
  });
345
381
  }
346
382
  static async fromStore(queue, id) {
@@ -359,6 +395,7 @@ var Job = class Job {
359
395
  const failedReason = (raw.failedReason ?? raw.lastErrorMessage) || void 0;
360
396
  const stacktrace = (raw.stacktrace ?? raw.lastErrorStack) || void 0;
361
397
  const returnvalue = raw.returnvalue ? safeJsonParse$1(raw.returnvalue) : void 0;
398
+ const parentId = raw.parentId || void 0;
362
399
  const [inProcessing, inDelayed] = await Promise.all([queue.redis.zscore(`${queue.namespace}:processing`, id), queue.redis.zscore(`${queue.namespace}:delayed`, id)]);
363
400
  let status = raw.status;
364
401
  if (inProcessing !== null) status = "active";
@@ -384,7 +421,8 @@ var Job = class Job {
384
421
  returnvalue,
385
422
  timestamp: timestampMs || Date.now(),
386
423
  orderMs,
387
- status: coerceStatus(status)
424
+ status: coerceStatus(status),
425
+ parentId
388
426
  });
389
427
  }
390
428
  };
@@ -632,6 +670,38 @@ var Queue = class {
632
670
  }
633
671
  return parsed;
634
672
  }
673
+ /**
674
+ * Gets all child job IDs for a parent job in a flow.
675
+ * @param parentId The ID of the parent job
676
+ * @returns An array of child job IDs
677
+ */
678
+ async getFlowChildrenIds(parentId) {
679
+ return this.r.smembers(`${this.ns}:flow:children:${parentId}`);
680
+ }
681
+ /**
682
+ * Gets all child jobs for a parent job in a flow.
683
+ * @param parentId The ID of the parent job
684
+ * @returns An array of Job instances for all children
685
+ */
686
+ async getFlowChildren(parentId) {
687
+ const ids = await this.getFlowChildrenIds(parentId);
688
+ if (ids.length === 0) return [];
689
+ const pipe = this.r.multi();
690
+ for (const id of ids) pipe.hgetall(`${this.ns}:job:${id}`);
691
+ const rows = await pipe.exec();
692
+ const jobs = [];
693
+ for (let i = 0; i < ids.length; i++) {
694
+ const id = ids[i];
695
+ const raw = rows?.[i]?.[1] || {};
696
+ if (!raw || Object.keys(raw).length === 0) {
697
+ this.logger.warn(`Skipping child job ${id} - not found (likely cleaned up)`);
698
+ continue;
699
+ }
700
+ const job = Job.fromRawHash(this, id, raw);
701
+ jobs.push(job);
702
+ }
703
+ return jobs;
704
+ }
635
705
  async addSingle(opts) {
636
706
  const now = Date.now();
637
707
  let delayUntil = 0;
@@ -2515,7 +2585,10 @@ var _Worker = class extends TypedEventEmitter {
2515
2585
  const oldest = Array.from(this.jobsInProgress)[0];
2516
2586
  const now = Date.now();
2517
2587
  return {
2518
- job: oldest.job,
2588
+ job: Job.fromReserved(this.q, oldest.job, {
2589
+ processedOn: oldest.ts,
2590
+ status: "active"
2591
+ }),
2519
2592
  processingTimeMs: now - oldest.ts
2520
2593
  };
2521
2594
  }
@@ -2525,7 +2598,10 @@ var _Worker = class extends TypedEventEmitter {
2525
2598
  getCurrentJobs() {
2526
2599
  const now = Date.now();
2527
2600
  return Array.from(this.jobsInProgress).map((item) => ({
2528
- job: item.job,
2601
+ job: Job.fromReserved(this.q, item.job, {
2602
+ processedOn: item.ts,
2603
+ status: "active"
2604
+ }),
2529
2605
  processingTimeMs: now - item.ts
2530
2606
  }));
2531
2607
  }
@@ -2555,7 +2631,7 @@ var _Worker = class extends TypedEventEmitter {
2555
2631
  } catch (e) {
2556
2632
  const isConnErr = this.q.isConnectionError(e);
2557
2633
  if (!isConnErr || !this.stopping) this.logger.error(`Heartbeat error for job ${job.id}:`, e instanceof Error ? e.message : String(e));
2558
- this.onError?.(e, job);
2634
+ this.onError?.(e, Job.fromReserved(this.q, job, { status: "active" }));
2559
2635
  if (!isConnErr || !this.stopping) this.emit("error", e instanceof Error ? e : new Error(String(e)));
2560
2636
  }
2561
2637
  }, minInterval);
@@ -2566,7 +2642,11 @@ var _Worker = class extends TypedEventEmitter {
2566
2642
  heartbeatDelayTimer = setTimeout(() => {
2567
2643
  startHeartbeat();
2568
2644
  }, heartbeatThreshold);
2569
- const handlerResult = await this.handler(job);
2645
+ const jobInstance = Job.fromReserved(this.q, job, {
2646
+ processedOn: jobStartWallTime,
2647
+ status: "active"
2648
+ });
2649
+ const handlerResult = await this.handler(jobInstance);
2570
2650
  if (heartbeatDelayTimer) clearTimeout(heartbeatDelayTimer);
2571
2651
  if (hbTimer) clearInterval(hbTimer);
2572
2652
  const finishedAtWall = Date.now();
@@ -2590,7 +2670,11 @@ var _Worker = class extends TypedEventEmitter {
2590
2670
  * Handle job failure: emit events, retry or dead-letter
2591
2671
  */
2592
2672
  async handleJobFailure(err, job, jobStartWallTime) {
2593
- this.onError?.(err, job);
2673
+ const jobInstance = Job.fromReserved(this.q, job, {
2674
+ processedOn: jobStartWallTime,
2675
+ status: "active"
2676
+ });
2677
+ this.onError?.(err, jobInstance);
2594
2678
  this.blockingStats.consecutiveEmptyReserves = 0;
2595
2679
  this.emptyReserveBackoffMs = 0;
2596
2680
  try {
@@ -2677,5 +2761,117 @@ function sleep(ms) {
2677
2761
  }
2678
2762
 
2679
2763
  //#endregion
2680
- export { BullBoardGroupMQAdapter, Job, Queue, UnrecoverableError, Worker, getWorkersStatus, waitForQueueToEmpty };
2764
+ //#region src/strategies/priority-strategy.ts
2765
+ var PriorityStrategy = class {
2766
+ constructor(options = {}) {
2767
+ this.cache = /* @__PURE__ */ new Map();
2768
+ this.overrides = /* @__PURE__ */ new Map();
2769
+ this.algorithmConfig = options.algorithm ?? { type: "weighted-random" };
2770
+ this.defaultPriority = options.defaultPriority ?? 1;
2771
+ this.cacheTtlMs = options.cacheTtlMs ?? 5e3;
2772
+ this.onGetPriority = options.onGetPriority;
2773
+ }
2774
+ /**
2775
+ * 手动覆盖某个组的优先级(优先级高于 Redis 配置和 getPriority)
2776
+ * 主要用于测试或临时调整
2777
+ */
2778
+ setPriority(groupId, priority) {
2779
+ this.overrides.set(groupId, priority);
2780
+ }
2781
+ /**
2782
+ * 清除手动覆盖,恢复使用 Redis 配置或 getPriority
2783
+ */
2784
+ clearPriority(groupId) {
2785
+ this.overrides.delete(groupId);
2786
+ }
2787
+ /**
2788
+ * 获取组的基础优先级
2789
+ * 优先级来源顺序:overrides > cache > onGetPriority(config) 或 config.priority
2790
+ */
2791
+ async resolvePriority(queue, groupId) {
2792
+ const override = this.overrides.get(groupId);
2793
+ if (override !== void 0) return override;
2794
+ const now = Date.now();
2795
+ const cached = this.cache.get(groupId);
2796
+ if (cached && cached.expiresAt > now) return cached.priority;
2797
+ const config = await queue.getGroupConfig(groupId);
2798
+ let priority;
2799
+ if (this.onGetPriority) priority = await this.onGetPriority(groupId, config);
2800
+ else priority = config.priority !== 1 ? config.priority : this.defaultPriority;
2801
+ if (this.cacheTtlMs > 0) this.cache.set(groupId, {
2802
+ priority,
2803
+ expiresAt: now + this.cacheTtlMs
2804
+ });
2805
+ return priority;
2806
+ }
2807
+ /**
2808
+ * 严格优先级算法:总是返回优先级最高的组
2809
+ */
2810
+ selectByStrict(groups) {
2811
+ groups.sort((a, b) => b.priority - a.priority);
2812
+ return groups[0].groupId;
2813
+ }
2814
+ /**
2815
+ * 加权随机算法:根据优先级计算概率选择
2816
+ * 使用 minWeightRatio 确保低优先级组也有机会
2817
+ */
2818
+ selectByWeightedRandom(groups) {
2819
+ if (groups.length === 1) return groups[0].groupId;
2820
+ const minWeightRatio = this.algorithmConfig.minWeightRatio ?? .1;
2821
+ const minWeight = Math.max(...groups.map((g) => g.priority)) * minWeightRatio;
2822
+ const weights = groups.map((g) => ({
2823
+ groupId: g.groupId,
2824
+ weight: Math.max(g.priority, minWeight)
2825
+ }));
2826
+ const totalWeight = weights.reduce((sum, w) => sum + w.weight, 0);
2827
+ let random = Math.random() * totalWeight;
2828
+ for (const w of weights) {
2829
+ random -= w.weight;
2830
+ if (random <= 0) return w.groupId;
2831
+ }
2832
+ return groups[0].groupId;
2833
+ }
2834
+ /**
2835
+ * 时间衰减算法:等待时间越长,优先级加成越高
2836
+ */
2837
+ selectByAging(groups) {
2838
+ const now = Date.now();
2839
+ const intervalMs = this.algorithmConfig.intervalMs ?? 6e4;
2840
+ const adjustedGroups = groups.map((g) => {
2841
+ let ageBonus = 0;
2842
+ if (g.oldestTimestamp) {
2843
+ const waitTime = now - g.oldestTimestamp;
2844
+ ageBonus = Math.floor(waitTime / intervalMs);
2845
+ }
2846
+ return {
2847
+ groupId: g.groupId,
2848
+ adjustedPriority: g.priority + ageBonus
2849
+ };
2850
+ });
2851
+ adjustedGroups.sort((a, b) => b.adjustedPriority - a.adjustedPriority);
2852
+ return adjustedGroups[0].groupId;
2853
+ }
2854
+ async getNextGroup(queue) {
2855
+ const readyGroups = await queue.getReadyGroups(0, 100);
2856
+ if (readyGroups.length === 0) return null;
2857
+ const groupInfos = await Promise.all(readyGroups.map(async (groupId) => {
2858
+ const priority = await this.resolvePriority(queue, groupId);
2859
+ const info = {
2860
+ groupId,
2861
+ priority
2862
+ };
2863
+ if (this.algorithmConfig.type === "aging") info.oldestTimestamp = await queue.getGroupOldestTimestamp(groupId);
2864
+ return info;
2865
+ }));
2866
+ switch (this.algorithmConfig.type) {
2867
+ case "strict": return this.selectByStrict(groupInfos);
2868
+ case "aging": return this.selectByAging(groupInfos);
2869
+ case "weighted-random":
2870
+ default: return this.selectByWeightedRandom(groupInfos);
2871
+ }
2872
+ }
2873
+ };
2874
+
2875
+ //#endregion
2876
+ export { BullBoardGroupMQAdapter, Job, PriorityStrategy, Queue, UnrecoverableError, Worker, getWorkersStatus, waitForQueueToEmpty };
2681
2877
  //# sourceMappingURL=index.js.map