@pattern-stack/codegen 0.8.1 → 0.9.1

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.
Files changed (120) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/{job-orchestrator.protocol-BwsBd37o.d.ts → job-orchestrator.protocol-CHOEqBDk.d.ts} +36 -1
  3. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.d.ts +2 -2
  4. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
  5. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
  6. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +5 -1
  7. package/dist/runtime/subsystems/bridge/bridge.module.js +930 -275
  8. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  9. package/dist/runtime/subsystems/bridge/event-flow.service.d.ts +1 -1
  10. package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
  11. package/dist/runtime/subsystems/bridge/index.d.ts +4 -1
  12. package/dist/runtime/subsystems/bridge/index.js +837 -182
  13. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  14. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +3 -1
  15. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +92 -1
  16. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
  17. package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +3 -1
  18. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +99 -0
  19. package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
  20. package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
  21. package/dist/runtime/subsystems/events/event-keyset-cursor.d.ts +32 -0
  22. package/dist/runtime/subsystems/events/event-keyset-cursor.js +38 -0
  23. package/dist/runtime/subsystems/events/event-keyset-cursor.js.map +1 -0
  24. package/dist/runtime/subsystems/events/event-read.protocol.d.ts +94 -0
  25. package/dist/runtime/subsystems/events/event-read.protocol.js +9 -0
  26. package/dist/runtime/subsystems/events/event-read.protocol.js.map +1 -0
  27. package/dist/runtime/subsystems/events/events.module.js +177 -3
  28. package/dist/runtime/subsystems/events/events.module.js.map +1 -1
  29. package/dist/runtime/subsystems/events/events.tokens.d.ts +16 -1
  30. package/dist/runtime/subsystems/events/events.tokens.js +2 -0
  31. package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
  32. package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
  33. package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
  34. package/dist/runtime/subsystems/events/index.d.ts +2 -1
  35. package/dist/runtime/subsystems/events/index.js +178 -3
  36. package/dist/runtime/subsystems/events/index.js.map +1 -1
  37. package/dist/runtime/subsystems/index.d.ts +3 -2
  38. package/dist/runtime/subsystems/index.js +1194 -264
  39. package/dist/runtime/subsystems/index.js.map +1 -1
  40. package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +98 -0
  41. package/dist/runtime/subsystems/jobs/bullmq.config.js +143 -0
  42. package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -0
  43. package/dist/runtime/subsystems/jobs/index.d.ts +8 -3
  44. package/dist/runtime/subsystems/jobs/index.js +861 -201
  45. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  46. package/dist/runtime/subsystems/jobs/job-handler.base.d.ts +2 -1
  47. package/dist/runtime/subsystems/jobs/job-handler.base.js.map +1 -1
  48. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +108 -0
  49. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +922 -0
  50. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -0
  51. package/dist/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.d.ts +2 -1
  52. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +2 -1
  53. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
  54. package/dist/runtime/subsystems/jobs/job-orchestrator.protocol.d.ts +2 -1
  55. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +53 -0
  56. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +57 -0
  57. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -0
  58. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +4 -2
  59. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +81 -1
  60. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
  61. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +4 -2
  62. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +81 -0
  63. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
  64. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +76 -2
  65. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +49 -0
  66. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +374 -0
  67. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -0
  68. package/dist/runtime/subsystems/jobs/job-worker.d.ts +2 -1
  69. package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
  70. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +44 -5
  71. package/dist/runtime/subsystems/jobs/job-worker.module.js +832 -178
  72. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  73. package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +10 -1
  74. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +519 -20
  75. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  76. package/dist/runtime/subsystems/jobs/jobs-errors.d.ts +2 -1
  77. package/dist/runtime/subsystems/jobs/pool-config.loader.d.ts +9 -1
  78. package/dist/runtime/subsystems/jobs/pool-config.loader.js +4 -0
  79. package/dist/runtime/subsystems/jobs/pool-config.loader.js.map +1 -1
  80. package/dist/runtime/subsystems/observability/index.d.ts +4 -3
  81. package/dist/runtime/subsystems/observability/index.js +109 -2
  82. package/dist/runtime/subsystems/observability/index.js.map +1 -1
  83. package/dist/runtime/subsystems/observability/observability.module.js +109 -2
  84. package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
  85. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +64 -3
  86. package/dist/runtime/subsystems/observability/observability.service.d.ts +22 -4
  87. package/dist/runtime/subsystems/observability/observability.service.js +109 -2
  88. package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
  89. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +3 -2
  90. package/dist/runtime/subsystems/observability/reporters/index.d.ts +3 -2
  91. package/dist/src/cli/index.js +30 -6
  92. package/dist/src/cli/index.js.map +1 -1
  93. package/package.json +5 -1
  94. package/runtime/subsystems/bridge/bridge.module.ts +5 -0
  95. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +109 -3
  96. package/runtime/subsystems/events/event-bus.memory-backend.ts +103 -1
  97. package/runtime/subsystems/events/event-keyset-cursor.ts +59 -0
  98. package/runtime/subsystems/events/event-read.protocol.ts +97 -0
  99. package/runtime/subsystems/events/events.module.ts +18 -2
  100. package/runtime/subsystems/events/events.tokens.ts +16 -0
  101. package/runtime/subsystems/events/index.ts +7 -0
  102. package/runtime/subsystems/jobs/bullmq.config.ts +125 -0
  103. package/runtime/subsystems/jobs/index.ts +22 -0
  104. package/runtime/subsystems/jobs/job-handler.base.ts +36 -0
  105. package/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts +381 -0
  106. package/runtime/subsystems/jobs/job-run-keyset-cursor.ts +88 -0
  107. package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +59 -1
  108. package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +53 -0
  109. package/runtime/subsystems/jobs/job-run-service.protocol.ts +77 -0
  110. package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +311 -0
  111. package/runtime/subsystems/jobs/job-worker.module.ts +124 -10
  112. package/runtime/subsystems/jobs/jobs-domain.module.ts +40 -21
  113. package/runtime/subsystems/jobs/pool-config.loader.ts +11 -0
  114. package/runtime/subsystems/observability/index.ts +8 -0
  115. package/runtime/subsystems/observability/observability.protocol.ts +76 -0
  116. package/runtime/subsystems/observability/observability.service.ts +148 -1
  117. package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -12
  118. package/templates/relationship/new/prompt.js +8 -5
  119. package/templates/subsystem/jobs/worker.ejs.t +30 -7
  120. package/templates/subsystem/sync/sync-audit.schema.ejs.t +12 -16
@@ -1,4 +1,7 @@
1
1
  import { DynamicModule } from '@nestjs/common';
2
+ import { BullMqExtensionsConfig } from './bullmq.config.js';
3
+ import 'bullmq';
4
+ import './pool-config.loader.js';
2
5
 
3
6
  /**
4
7
  * JobsDomainModule — `DynamicModule.forRoot({ backend })` factory wiring
@@ -32,7 +35,7 @@ interface DrizzleBackendExtensions {
32
35
  pollIntervalMs?: number;
33
36
  }
34
37
  interface JobsDomainModuleOptions {
35
- backend: 'drizzle' | 'memory';
38
+ backend: 'drizzle' | 'memory' | 'bullmq';
36
39
  /**
37
40
  * Backend-specific extensions. Only the matching backend's extensions
38
41
  * are read at boot; non-matching keys are ignored. This is the
@@ -40,6 +43,12 @@ interface JobsDomainModuleOptions {
40
43
  */
41
44
  extensions?: {
42
45
  drizzle?: DrizzleBackendExtensions;
46
+ /**
47
+ * BullMQ backend extensions (BULLMQ-1). Snake_case mirrors the YAML
48
+ * under `jobs.extensions.bullmq`. `redis_url` falls back to
49
+ * `process.env.REDIS_URL` then `redis://localhost:6379`.
50
+ */
51
+ bullmq?: BullMqExtensionsConfig;
43
52
  };
44
53
  /** Multi-tenancy opt-in. Wired by JOB-8; module signature stays stable. */
45
54
  multiTenant?: boolean;
@@ -565,7 +565,58 @@ function notInStatus(statuses) {
565
565
 
566
566
  // runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
567
567
  import { Inject as Inject2, Injectable as Injectable2 } from "@nestjs/common";
568
- import { and as and2, asc, desc as desc2, eq as eq2, inArray as inArray2, isNull, sql as sql3 } from "drizzle-orm";
568
+ import { and as and2, asc, desc as desc2, eq as eq2, gte, inArray as inArray2, isNull, lt, or, sql as sql3 } from "drizzle-orm";
569
+
570
+ // runtime/subsystems/jobs/job-run-keyset-cursor.ts
571
+ var DEFAULT_LIST_LIMIT = 50;
572
+ var MAX_LIST_LIMIT = 200;
573
+ function clampLimit(limit) {
574
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
575
+ return DEFAULT_LIST_LIMIT;
576
+ }
577
+ const floored = Math.floor(limit);
578
+ if (floored < 1) return 1;
579
+ if (floored > MAX_LIST_LIMIT) return MAX_LIST_LIMIT;
580
+ return floored;
581
+ }
582
+ function encodeKeysetCursor(keyset) {
583
+ const tuple = [keyset.createdAt.toISOString(), keyset.id];
584
+ return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
585
+ }
586
+ function decodeKeysetCursor(cursor) {
587
+ try {
588
+ const json = Buffer.from(cursor, "base64url").toString("utf8");
589
+ const parsed = JSON.parse(json);
590
+ if (!Array.isArray(parsed) || parsed.length !== 2) return null;
591
+ const [iso, id] = parsed;
592
+ if (typeof iso !== "string" || typeof id !== "string") return null;
593
+ const createdAt = new Date(iso);
594
+ if (Number.isNaN(createdAt.getTime())) return null;
595
+ return { createdAt, id };
596
+ } catch {
597
+ return null;
598
+ }
599
+ }
600
+ function toJobRunSummary(r) {
601
+ return {
602
+ runId: r.id,
603
+ rootRunId: r.rootRunId,
604
+ jobType: r.jobType,
605
+ pool: r.pool,
606
+ status: r.status,
607
+ scopeEntityType: r.scopeEntityType,
608
+ scopeEntityId: r.scopeEntityId,
609
+ tenantId: r.tenantId,
610
+ attempts: r.attempts,
611
+ errorMessage: r.error?.message ?? null,
612
+ runAt: r.runAt,
613
+ startedAt: r.startedAt,
614
+ finishedAt: r.finishedAt,
615
+ createdAt: r.createdAt
616
+ };
617
+ }
618
+
619
+ // runtime/subsystems/jobs/job-run-service.drizzle-backend.ts
569
620
  var NON_TERMINAL_STATUSES = [
570
621
  "pending",
571
622
  "running",
@@ -688,6 +739,37 @@ var DrizzleJobRunService = class {
688
739
  createdAt: r.createdAt
689
740
  }));
690
741
  }
742
+ async listJobRuns(query = {}) {
743
+ const limit = clampLimit(query.limit);
744
+ const conditions = [];
745
+ const tenantCond = this.tenantCondition("listJobRuns", query.tenantId);
746
+ if (tenantCond) conditions.push(tenantCond);
747
+ if (query.poolId) conditions.push(eq2(jobRuns.pool, query.poolId));
748
+ if (query.rootRunId) conditions.push(eq2(jobRuns.rootRunId, query.rootRunId));
749
+ if (query.status) conditions.push(eq2(jobRuns.status, query.status));
750
+ if (query.since) conditions.push(gte(jobRuns.createdAt, query.since));
751
+ if (query.cursor) {
752
+ const keyset = decodeKeysetCursor(query.cursor);
753
+ if (keyset) {
754
+ conditions.push(
755
+ or(
756
+ lt(jobRuns.createdAt, keyset.createdAt),
757
+ and2(
758
+ eq2(jobRuns.createdAt, keyset.createdAt),
759
+ lt(jobRuns.id, keyset.id)
760
+ )
761
+ )
762
+ );
763
+ }
764
+ }
765
+ const rows = await this.db.select().from(jobRuns).where(conditions.length > 0 ? and2(...conditions) : void 0).orderBy(desc2(jobRuns.createdAt), desc2(jobRuns.id)).limit(limit + 1);
766
+ const hasMore = rows.length > limit;
767
+ const page = hasMore ? rows.slice(0, limit) : rows;
768
+ const items = page.map(toJobRunSummary);
769
+ const last = page[page.length - 1];
770
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
771
+ return { items, nextCursor };
772
+ }
691
773
  /**
692
774
  * Internal helper used by cascade paths (not on the public protocol).
693
775
  * Exposed as a public method on the concrete class so infrastructure
@@ -749,9 +831,384 @@ DrizzleJobStepService = __decorateClass([
749
831
  __decorateParam(0, Inject3(DRIZZLE))
750
832
  ], DrizzleJobStepService);
751
833
 
834
+ // runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
835
+ import { createHash } from "crypto";
836
+ import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
837
+ import { eq as eq4 } from "drizzle-orm";
838
+
839
+ // runtime/subsystems/jobs/pool-config.loader.ts
840
+ import { existsSync, readFileSync } from "fs";
841
+ import { resolve } from "path";
842
+ import { parse as parseYaml } from "yaml";
843
+ var FRAMEWORK_POOLS = Object.freeze({
844
+ events_inbound: Object.freeze({
845
+ queue: "jobs-events-inbound",
846
+ concurrency: 20,
847
+ reserved: true,
848
+ description: "Inbound events drain (events subsystem outbox)."
849
+ }),
850
+ events_change: Object.freeze({
851
+ queue: "jobs-events-change",
852
+ concurrency: 30,
853
+ reserved: true,
854
+ description: "Change events drain (events subsystem outbox)."
855
+ }),
856
+ events_outbound: Object.freeze({
857
+ queue: "jobs-events-outbound",
858
+ concurrency: 10,
859
+ reserved: true,
860
+ description: "Outbound events drain (events subsystem outbox)."
861
+ }),
862
+ interactive: Object.freeze({
863
+ queue: "jobs-interactive",
864
+ concurrency: 20,
865
+ reserved: false,
866
+ description: "User-facing latency-sensitive jobs."
867
+ }),
868
+ batch: Object.freeze({
869
+ queue: "jobs-batch",
870
+ concurrency: 5,
871
+ reserved: false,
872
+ description: "Default pool for background jobs."
873
+ })
874
+ });
875
+ var RESERVED_POOL_NAMES = new Set(
876
+ Object.entries(FRAMEWORK_POOLS).filter(([, def]) => def.reserved).map(([name]) => name)
877
+ );
878
+ var cache = /* @__PURE__ */ new Map();
879
+ function loadPoolConfig(configPath) {
880
+ const resolved = resolve(configPath ?? `${process.cwd()}/codegen.config.yaml`);
881
+ const cached = cache.get(resolved);
882
+ if (cached) return cached;
883
+ const merged = /* @__PURE__ */ new Map();
884
+ for (const [name, def] of Object.entries(FRAMEWORK_POOLS)) {
885
+ merged.set(name, { ...def });
886
+ }
887
+ if (!existsSync(resolved)) {
888
+ cache.set(resolved, merged);
889
+ return merged;
890
+ }
891
+ let raw;
892
+ try {
893
+ raw = parseYaml(readFileSync(resolved, "utf8"));
894
+ } catch (err) {
895
+ throw new Error(
896
+ `pool-config.loader: failed to parse YAML at ${resolved}: ${err.message}`
897
+ );
898
+ }
899
+ const userPools = extractUserPools(raw);
900
+ for (const [name, userDef] of Object.entries(userPools)) {
901
+ const existing = merged.get(name);
902
+ if (existing) {
903
+ const next = {
904
+ queue: existing.queue,
905
+ concurrency: typeof userDef.concurrency === "number" ? userDef.concurrency : existing.concurrency,
906
+ reserved: existing.reserved,
907
+ description: userDef.description ?? existing.description
908
+ };
909
+ merged.set(name, next);
910
+ continue;
911
+ }
912
+ if (typeof userDef.queue !== "string" || userDef.queue.length === 0) {
913
+ throw new Error(
914
+ `pool-config.loader: pool '${name}' must declare a non-empty 'queue'.`
915
+ );
916
+ }
917
+ if (typeof userDef.concurrency !== "number" || userDef.concurrency <= 0) {
918
+ throw new Error(
919
+ `pool-config.loader: pool '${name}' must declare a positive 'concurrency'.`
920
+ );
921
+ }
922
+ if (userDef.reserved === true) {
923
+ throw new Error(
924
+ `pool-config.loader: user-defined pool '${name}' cannot set 'reserved: true' \u2014 reserved is framework-only.`
925
+ );
926
+ }
927
+ merged.set(name, {
928
+ queue: userDef.queue,
929
+ concurrency: userDef.concurrency,
930
+ reserved: false,
931
+ description: userDef.description
932
+ });
933
+ }
934
+ cache.set(resolved, merged);
935
+ return merged;
936
+ }
937
+ function extractUserPools(raw) {
938
+ if (!raw || typeof raw !== "object") return {};
939
+ const jobs2 = raw.jobs;
940
+ if (!jobs2 || typeof jobs2 !== "object") return {};
941
+ const pools = jobs2.pools;
942
+ if (!pools || typeof pools !== "object") return {};
943
+ const out = {};
944
+ for (const [name, def] of Object.entries(pools)) {
945
+ if (!def || typeof def !== "object") continue;
946
+ out[name] = def;
947
+ }
948
+ return out;
949
+ }
950
+
951
+ // runtime/subsystems/jobs/bullmq.config.ts
952
+ var BULLMQ_CONNECTION = /* @__PURE__ */ Symbol("BULLMQ_CONNECTION");
953
+ var BULLMQ_RESOLVED_CONFIG = /* @__PURE__ */ Symbol("BULLMQ_RESOLVED_CONFIG");
954
+ var DEFAULT_REDIS_URL = "redis://localhost:6379";
955
+ var DEFAULT_BULL_BOARD_MOUNT = "/admin/queues";
956
+ function resolveBullMqConfig(ext) {
957
+ const url = ext?.redis_url ?? process.env.REDIS_URL ?? DEFAULT_REDIS_URL;
958
+ const resolved = {
959
+ connection: { url },
960
+ queuePrefix: ext?.queue_prefix
961
+ };
962
+ if (ext?.bull_board?.enabled) {
963
+ resolved.bullBoard = {
964
+ enabled: true,
965
+ mountPath: ext.bull_board.mount_path ?? DEFAULT_BULL_BOARD_MOUNT
966
+ };
967
+ }
968
+ return resolved;
969
+ }
970
+ function resolvePoolQueueName(pool, config, poolConfig = loadPoolConfig()) {
971
+ const alias = poolConfig.get(pool)?.queue ?? pool;
972
+ const prefix = config?.queuePrefix;
973
+ return prefix ? `${prefix}:${alias}` : alias;
974
+ }
975
+
976
+ // runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts
977
+ function sha1JobId(idempotencyKey) {
978
+ return createHash("sha1").update(idempotencyKey).digest("hex");
979
+ }
980
+ var BullMQJobOrchestrator = class extends DrizzleJobOrchestrator {
981
+ constructor(db, multiTenant, connection, bullConfig = null) {
982
+ super(db, multiTenant);
983
+ this.connection = connection;
984
+ this.bullConfig = bullConfig;
985
+ this.bullDb = db;
986
+ }
987
+ connection;
988
+ bullConfig;
989
+ // TODO(logging-subsystem): swap to ILogger once ADR-028 lands
990
+ bullLogger = new Logger2(BullMQJobOrchestrator.name);
991
+ /** Lazily-opened `Queue` handles, one per pool. */
992
+ queues = /* @__PURE__ */ new Map();
993
+ /** Single FlowProducer for parent/child hierarchies. Lazily opened. */
994
+ _flow = null;
995
+ /**
996
+ * Cached `bullmq` value constructors, populated by `loadBullMq()` on first
997
+ * use (the `start`/`cancel`/`replay` entrypoints `await` it before touching
998
+ * a queue). Kept off the import graph so a `drizzle`-only consumer never
999
+ * resolves the optional `'bullmq'` package.
1000
+ */
1001
+ QueueCtor = null;
1002
+ FlowProducerCtor = null;
1003
+ bullMqLoad = null;
1004
+ /**
1005
+ * Own reference to the Drizzle client. `DrizzleJobOrchestrator.db` is
1006
+ * `private` (can't be redeclared even privately in a subclass), and the
1007
+ * spec forbids touching that file — so the subclass keeps its own handle
1008
+ * under a distinct name (same instance, passed through to `super`) for the
1009
+ * cancel-cascade snapshot + definition/run loads below.
1010
+ */
1011
+ bullDb;
1012
+ /**
1013
+ * Lazily load the optional `bullmq` package and cache its value
1014
+ * constructors. Idempotent (single in-flight promise). Throws a friendly,
1015
+ * actionable error when the consumer selected `backend: 'bullmq'` but did
1016
+ * not install the package — mirrors `createRedisClient` in the redis event
1017
+ * backend. Must be `await`ed before any `queueFor`/`flow` access.
1018
+ */
1019
+ async loadBullMq() {
1020
+ if (this.QueueCtor && this.FlowProducerCtor) return;
1021
+ if (!this.bullMqLoad) {
1022
+ this.bullMqLoad = (async () => {
1023
+ try {
1024
+ const mod = await import("bullmq");
1025
+ this.QueueCtor = mod.Queue;
1026
+ this.FlowProducerCtor = mod.FlowProducer;
1027
+ } catch {
1028
+ throw new Error(
1029
+ 'BullMQ backend requires the "bullmq" package. Install it with: npm install bullmq'
1030
+ );
1031
+ }
1032
+ })();
1033
+ }
1034
+ await this.bullMqLoad;
1035
+ }
1036
+ /**
1037
+ * Open (or reuse) the `Queue` for a pool. Synchronous — callers `await
1038
+ * loadBullMq()` first so `QueueCtor` is populated.
1039
+ */
1040
+ queueFor(pool) {
1041
+ if (!this.QueueCtor) {
1042
+ throw new Error("BullMQJobOrchestrator: queueFor called before loadBullMq()");
1043
+ }
1044
+ const name = resolvePoolQueueName(pool, this.bullConfig);
1045
+ let q = this.queues.get(name);
1046
+ if (!q) {
1047
+ q = new this.QueueCtor(name, { connection: this.connection });
1048
+ this.queues.set(name, q);
1049
+ }
1050
+ return q;
1051
+ }
1052
+ flow() {
1053
+ if (!this.FlowProducerCtor) {
1054
+ throw new Error("BullMQJobOrchestrator: flow called before loadBullMq()");
1055
+ }
1056
+ if (!this._flow) {
1057
+ this._flow = new this.FlowProducerCtor({ connection: this.connection });
1058
+ }
1059
+ return this._flow;
1060
+ }
1061
+ // ==========================================================================
1062
+ // start — Postgres insert (super) + BullMQ dispatch
1063
+ // ==========================================================================
1064
+ async start(type, input, opts = {}, tx) {
1065
+ const run = await super.start(type, input, opts, tx);
1066
+ await this.dispatch(run, type);
1067
+ return run;
1068
+ }
1069
+ /**
1070
+ * Map a `job_run` row onto a BullMQ job via `queue.add`. When the run has a
1071
+ * `parentRunId` we attach it to the parent's existing BullMQ job through the
1072
+ * `parent: { id, queue }` opt — BullMQ then tracks the parent/child link in
1073
+ * its own graph. (The FlowProducer is reserved for whole-tree atomic
1074
+ * submits, exposed as an opt-in extension via `flowProducer()`; runtime
1075
+ * `ctx.spawnChild` is incremental, so `queue.add` with a parent ref is the
1076
+ * correct primitive here.)
1077
+ *
1078
+ * The `jobId` is colon-safe + stable: `sha1(dedupeKey)` when a dedupe key is
1079
+ * present (so the same logical key dedups), else the `job_run.id` UUID
1080
+ * (already colon-free).
1081
+ *
1082
+ * The domain `parentClosePolicy` cascade is still enforced in Postgres by
1083
+ * the shared `cancel` path — BullMQ's parent link is dispatch bookkeeping,
1084
+ * not the authority.
1085
+ */
1086
+ async dispatch(run, type) {
1087
+ await this.loadBullMq();
1088
+ const def = await this.loadDefinition(type);
1089
+ const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
1090
+ const jobOpts = {
1091
+ jobId,
1092
+ ...this.retryOpts(def),
1093
+ ...this.dedupeOpts(run, def)
1094
+ };
1095
+ if (run.parentRunId) {
1096
+ const parentRow = await this.loadRun(run.parentRunId);
1097
+ if (parentRow) {
1098
+ const parentJobId = parentRow.dedupeKey ? sha1JobId(parentRow.dedupeKey) : parentRow.id;
1099
+ jobOpts.parent = {
1100
+ id: parentJobId,
1101
+ queue: resolvePoolQueueName(parentRow.pool, this.bullConfig)
1102
+ };
1103
+ }
1104
+ }
1105
+ const payload = { runId: run.id, type, input: run.input };
1106
+ await this.queueFor(run.pool).add(type, payload, jobOpts);
1107
+ }
1108
+ /**
1109
+ * Opt-in extension (spec §Extensions): expose the FlowProducer for
1110
+ * consumers that want to submit a whole parent/child DAG atomically up
1111
+ * front, rather than incrementally via `ctx.spawnChild`. Backend-specific —
1112
+ * code using it is not portable to the Drizzle backend. Async because it
1113
+ * lazily loads the optional `bullmq` package on first use.
1114
+ */
1115
+ async flowProducer() {
1116
+ await this.loadBullMq();
1117
+ return this.flow();
1118
+ }
1119
+ retryOpts(def) {
1120
+ const policy = def.retryPolicy;
1121
+ if (!policy) return {};
1122
+ return {
1123
+ attempts: policy.attempts,
1124
+ backoff: {
1125
+ type: policy.backoff === "exponential" ? "exponential" : "fixed",
1126
+ delay: policy.baseMs
1127
+ }
1128
+ };
1129
+ }
1130
+ dedupeOpts(run, def) {
1131
+ if (!run.dedupeKey || !def.dedupeWindowMs) return {};
1132
+ return {
1133
+ deduplication: {
1134
+ id: sha1JobId(run.dedupeKey),
1135
+ ttl: def.dedupeWindowMs
1136
+ }
1137
+ };
1138
+ }
1139
+ // ==========================================================================
1140
+ // cancel — Postgres cascade (super) + remove from queue
1141
+ // ==========================================================================
1142
+ async cancel(runId, opts = {}) {
1143
+ const target = await this.loadRun(runId);
1144
+ await super.cancel(runId, opts);
1145
+ if (!target) return;
1146
+ await this.loadBullMq();
1147
+ await this.removeFromQueue(target);
1148
+ if (opts.cascade === false) return;
1149
+ const descendants = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.rootRunId, target.rootRunId));
1150
+ for (const child of descendants) {
1151
+ if (child.id === runId) continue;
1152
+ await this.removeFromQueue(child);
1153
+ }
1154
+ }
1155
+ async removeFromQueue(run) {
1156
+ const jobId = run.dedupeKey ? sha1JobId(run.dedupeKey) : run.id;
1157
+ try {
1158
+ const job = await this.queueFor(run.pool).getJob(jobId);
1159
+ if (job) await job.remove();
1160
+ } catch (err) {
1161
+ this.bullLogger.warn(
1162
+ `cancel: could not remove BullMQ job ${jobId} (pool=${run.pool}): ${err.message}`
1163
+ );
1164
+ }
1165
+ }
1166
+ // ==========================================================================
1167
+ // replay — Postgres reset (super) + re-enqueue
1168
+ // ==========================================================================
1169
+ async replay(runId) {
1170
+ const run = await super.replay(runId);
1171
+ await this.dispatch(run, run.jobType);
1172
+ return run;
1173
+ }
1174
+ // ==========================================================================
1175
+ // Internals
1176
+ // ==========================================================================
1177
+ async loadDefinition(type) {
1178
+ const [def] = await this.bullDb.select().from(jobs).where(eq4(jobs.type, type)).limit(1);
1179
+ if (!def) {
1180
+ throw new Error(`BullMQJobOrchestrator: no job definition for '${type}'`);
1181
+ }
1182
+ return def;
1183
+ }
1184
+ async loadRun(id) {
1185
+ const [row] = await this.bullDb.select().from(jobRuns).where(eq4(jobRuns.id, id)).limit(1);
1186
+ return row ?? null;
1187
+ }
1188
+ /** Close all open queue + flow connections. Called on module destroy. */
1189
+ async closeConnections() {
1190
+ for (const q of this.queues.values()) {
1191
+ await q.close().catch(() => void 0);
1192
+ }
1193
+ this.queues.clear();
1194
+ if (this._flow) {
1195
+ await this._flow.close().catch(() => void 0);
1196
+ this._flow = null;
1197
+ }
1198
+ }
1199
+ };
1200
+ BullMQJobOrchestrator = __decorateClass([
1201
+ Injectable4(),
1202
+ __decorateParam(0, Inject4(DRIZZLE)),
1203
+ __decorateParam(1, Inject4(JOBS_MULTI_TENANT)),
1204
+ __decorateParam(2, Inject4(BULLMQ_CONNECTION)),
1205
+ __decorateParam(3, Optional()),
1206
+ __decorateParam(3, Inject4(BULLMQ_RESOLVED_CONFIG))
1207
+ ], BullMQJobOrchestrator);
1208
+
752
1209
  // runtime/subsystems/jobs/job-orchestrator.memory-backend.ts
753
1210
  import { randomUUID as randomUUID2 } from "crypto";
754
- import { Inject as Inject4, Injectable as Injectable4, Logger as Logger2, Optional } from "@nestjs/common";
1211
+ import { Inject as Inject5, Injectable as Injectable5, Logger as Logger3, Optional as Optional2 } from "@nestjs/common";
755
1212
 
756
1213
  // runtime/subsystems/jobs/job-handler.base.ts
757
1214
  var JOB_HANDLER_REGISTRY = /* @__PURE__ */ new Map();
@@ -814,7 +1271,7 @@ var MemoryJobOrchestrator = class {
814
1271
  stepService;
815
1272
  multiTenant;
816
1273
  moduleRef;
817
- logger = new Logger2(MemoryJobOrchestrator.name);
1274
+ logger = new Logger3(MemoryJobOrchestrator.name);
818
1275
  mutex = new PromiseMutex();
819
1276
  handlerRegistry = /* @__PURE__ */ new Map();
820
1277
  /**
@@ -1163,7 +1620,7 @@ var MemoryJobOrchestrator = class {
1163
1620
  run,
1164
1621
  step: this.makeStepFn(run),
1165
1622
  spawnChild: this.makeSpawnFn(run),
1166
- logger: new Logger2(`JobRun:${run.id}`)
1623
+ logger: new Logger3(`JobRun:${run.id}`)
1167
1624
  };
1168
1625
  const attemptsBefore = run.attempts ?? 0;
1169
1626
  try {
@@ -1336,9 +1793,9 @@ var MemoryJobOrchestrator = class {
1336
1793
  }
1337
1794
  };
1338
1795
  MemoryJobOrchestrator = __decorateClass([
1339
- Injectable4(),
1340
- __decorateParam(2, Inject4(JOBS_MULTI_TENANT)),
1341
- __decorateParam(3, Optional())
1796
+ Injectable5(),
1797
+ __decorateParam(2, Inject5(JOBS_MULTI_TENANT)),
1798
+ __decorateParam(3, Optional2())
1342
1799
  ], MemoryJobOrchestrator);
1343
1800
  function classifyError(err, policy, currentAttempts) {
1344
1801
  if (!policy) return "fail";
@@ -1372,7 +1829,7 @@ function serialiseError(err, attempt, retryable) {
1372
1829
  }
1373
1830
 
1374
1831
  // runtime/subsystems/jobs/job-run-service.memory-backend.ts
1375
- import { Inject as Inject5, Injectable as Injectable5 } from "@nestjs/common";
1832
+ import { Inject as Inject6, Injectable as Injectable6 } from "@nestjs/common";
1376
1833
  var NON_TERMINAL_STATUSES2 = [
1377
1834
  "pending",
1378
1835
  "running",
@@ -1489,6 +1946,38 @@ var MemoryJobRunService = class {
1489
1946
  createdAt: r.createdAt
1490
1947
  }));
1491
1948
  }
1949
+ async listJobRuns(query = {}) {
1950
+ const limit = clampLimit(query.limit);
1951
+ const tenantCheck = this.tenantPredicate("listJobRuns", query.tenantId);
1952
+ const keyset = query.cursor ? decodeKeysetCursor(query.cursor) : null;
1953
+ const matched = [];
1954
+ for (const r of this.store.runs.values()) {
1955
+ if (tenantCheck && !tenantCheck(r)) continue;
1956
+ if (query.poolId && r.pool !== query.poolId) continue;
1957
+ if (query.rootRunId && r.rootRunId !== query.rootRunId) continue;
1958
+ if (query.status && r.status !== query.status) continue;
1959
+ if (query.since && r.createdAt.getTime() < query.since.getTime()) continue;
1960
+ matched.push(r);
1961
+ }
1962
+ matched.sort((a, b) => {
1963
+ const dt = b.createdAt.getTime() - a.createdAt.getTime();
1964
+ if (dt !== 0) return dt;
1965
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
1966
+ });
1967
+ const seeked = keyset ? matched.filter((r) => {
1968
+ const ct = r.createdAt.getTime();
1969
+ const kt = keyset.createdAt.getTime();
1970
+ if (ct < kt) return true;
1971
+ if (ct > kt) return false;
1972
+ return r.id < keyset.id;
1973
+ }) : matched;
1974
+ const hasMore = seeked.length > limit;
1975
+ const page = hasMore ? seeked.slice(0, limit) : seeked;
1976
+ const items = page.map(toJobRunSummary);
1977
+ const last = page[page.length - 1];
1978
+ const nextCursor = hasMore && last ? encodeKeysetCursor({ createdAt: last.createdAt, id: last.id }) : null;
1979
+ return { items, nextCursor };
1980
+ }
1492
1981
  /**
1493
1982
  * Direct lookup. Not on the protocol — concrete-class convenience for
1494
1983
  * tests. Matches `DrizzleJobRunService.findByRootRunId` in spirit; both
@@ -1507,9 +1996,9 @@ var MemoryJobRunService = class {
1507
1996
  }
1508
1997
  };
1509
1998
  MemoryJobRunService = __decorateClass([
1510
- Injectable5(),
1511
- __decorateParam(1, Inject5(JOB_ORCHESTRATOR)),
1512
- __decorateParam(2, Inject5(JOBS_MULTI_TENANT))
1999
+ Injectable6(),
2000
+ __decorateParam(1, Inject6(JOB_ORCHESTRATOR)),
2001
+ __decorateParam(2, Inject6(JOBS_MULTI_TENANT))
1513
2002
  ], MemoryJobRunService);
1514
2003
  function compareBy(a, b, order) {
1515
2004
  switch (order) {
@@ -1527,7 +2016,7 @@ function compareBy(a, b, order) {
1527
2016
 
1528
2017
  // runtime/subsystems/jobs/job-step-service.memory-backend.ts
1529
2018
  import { randomUUID as randomUUID3 } from "crypto";
1530
- import { Injectable as Injectable6 } from "@nestjs/common";
2019
+ import { Injectable as Injectable7 } from "@nestjs/common";
1531
2020
  var MemoryJobStepService = class {
1532
2021
  constructor(store) {
1533
2022
  this.store = store;
@@ -1620,7 +2109,7 @@ var MemoryJobStepService = class {
1620
2109
  }
1621
2110
  };
1622
2111
  MemoryJobStepService = __decorateClass([
1623
- Injectable6()
2112
+ Injectable7()
1624
2113
  ], MemoryJobStepService);
1625
2114
 
1626
2115
  // runtime/subsystems/jobs/memory-job-store.ts
@@ -1642,7 +2131,6 @@ var MemoryJobStore = class {
1642
2131
  // runtime/subsystems/jobs/jobs-domain.module.ts
1643
2132
  var JobsDomainModule = class {
1644
2133
  static forRoot(opts) {
1645
- void opts.extensions;
1646
2134
  const multiTenant = opts.multiTenant ?? false;
1647
2135
  const providers = [
1648
2136
  // JOB-8 — boolean provider consumed by the four service-layer backends.
@@ -1661,21 +2149,32 @@ var JobsDomainModule = class {
1661
2149
  providers.push({ provide: JOB_ORCHESTRATOR, useExisting: MemoryJobOrchestrator });
1662
2150
  providers.push(MemoryJobRunService);
1663
2151
  providers.push({ provide: JOB_RUN_SERVICE, useExisting: MemoryJobRunService });
2152
+ } else if (opts.backend === "bullmq") {
2153
+ const resolved = resolveBullMqConfig(opts.extensions?.bullmq);
2154
+ providers.push({ provide: BULLMQ_CONNECTION, useValue: resolved.connection });
2155
+ providers.push({ provide: BULLMQ_RESOLVED_CONFIG, useValue: resolved });
2156
+ providers.push({ provide: JOB_ORCHESTRATOR, useClass: BullMQJobOrchestrator });
2157
+ providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
2158
+ providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
1664
2159
  } else {
1665
2160
  providers.push({ provide: JOB_ORCHESTRATOR, useClass: DrizzleJobOrchestrator });
1666
2161
  providers.push({ provide: JOB_RUN_SERVICE, useClass: DrizzleJobRunService });
1667
2162
  providers.push({ provide: JOB_STEP_SERVICE, useClass: DrizzleJobStepService });
1668
2163
  }
2164
+ const exports = [
2165
+ JOB_ORCHESTRATOR,
2166
+ JOB_RUN_SERVICE,
2167
+ JOB_STEP_SERVICE,
2168
+ JOBS_MULTI_TENANT
2169
+ ];
2170
+ if (opts.backend === "bullmq") {
2171
+ exports.push(BULLMQ_CONNECTION, BULLMQ_RESOLVED_CONFIG);
2172
+ }
1669
2173
  return {
1670
2174
  module: JobsDomainModule,
1671
2175
  global: true,
1672
2176
  providers,
1673
- exports: [
1674
- JOB_ORCHESTRATOR,
1675
- JOB_RUN_SERVICE,
1676
- JOB_STEP_SERVICE,
1677
- JOBS_MULTI_TENANT
1678
- ]
2177
+ exports
1679
2178
  };
1680
2179
  }
1681
2180
  };