package ports import ( "context" "fmt" "strings" ) // JobResultPublisher emits one entry on the `runtime:job_results` Redis // Stream per finalised start or stop runtime job. Adapters serialise // every JobResult field verbatim so consumers (Game Lobby's // runtime-job-result worker today, future services tomorrow) see the // AsyncAPI shape frozen in `rtmanager/api/runtime-jobs-asyncapi.yaml`. // // The start-jobs and stop-jobs consumers publish through this port. // The synchronous REST handlers do not — REST callers receive the same // `Result` shape directly from the service layer. type JobResultPublisher interface { // Publish records result on the configured `runtime:job_results` // stream. A non-nil error reports a transport or serialisation // failure; the caller treats the failure as a degraded emission // (the operation_log already records the durable outcome). Publish(ctx context.Context, result JobResult) error } // JobResult outcome values frozen by the // `RuntimeJobResultPayload.outcome` enum. const ( // JobOutcomeSuccess marks a successful start or stop, including the // idempotent replay variant (`error_code=replay_no_op`). JobOutcomeSuccess = "success" // JobOutcomeFailure marks a stable failure for which the payload // carries a non-empty `error_code`. JobOutcomeFailure = "failure" ) // JobResult carries the wire payload published on // `runtime:job_results`. The fields mirror the AsyncAPI schema frozen // in `rtmanager/api/runtime-jobs-asyncapi.yaml`; adapters serialise // every field verbatim so consumers see the contracted shape. Fields // that are required by the contract (every field on this struct) are // always present in the wire entry — even when their string value is // empty (allowed for `container_id` / `engine_endpoint` / `error_code` // / `error_message` on appropriate variants). type JobResult struct { // GameID identifies the platform game the job acted on. Required. GameID string // Outcome reports the high-level outcome. Must be `success` or // `failure` (use the JobOutcome* constants). Outcome string // ContainerID stores the Docker container id. Populated on // `success` for fresh starts and replays; empty on `failure` and // on `success/replay_no_op` for stop jobs that observed a removed // record. ContainerID string // EngineEndpoint stores the stable engine URL // `http://galaxy-game-{game_id}:8080`. Populated alongside // ContainerID, empty in the same cases. EngineEndpoint string // ErrorCode stores the stable error code from // `rtmanager/README.md §Error Model`. Empty for fresh successes, // `replay_no_op` for idempotent replays, one of the failure // codes otherwise. ErrorCode string // ErrorMessage stores the operator-readable detail. Empty for // successes; populated alongside ErrorCode on failure. ErrorMessage string } // Validate reports whether result satisfies the structural invariants // implied by the AsyncAPI schema: a non-empty game id and one of the // two known outcome values. The remaining fields are required to be // present on the wire but may be empty strings, so Validate does not // constrain them. func (result JobResult) Validate() error { if strings.TrimSpace(result.GameID) == "" { return fmt.Errorf("job result: game id must not be empty") } switch result.Outcome { case JobOutcomeSuccess, JobOutcomeFailure: return nil default: return fmt.Errorf("job result: outcome %q is unsupported", result.Outcome) } }