azure-sdk-for-js/sdk/communication/communication-job-router-rest
Matthew Podwysocki 10ab5dc4a8
[communication] Migrate @azure-rest/communication-job-router to ESM/vitest (#31774)
### Packages impacted by this PR

- @azure-rest/communication-job-router

### Issues associated with this PR

- https://github.com/Azure/azure-sdk-for-js/issues/31338

### Describe the problem that is addressed by this PR

Migrates the @azure-rest/communication-job-router package to ESM and
vitest.

### What are the possible designs available to address the problem? If
there are more than one possible design, why was the one in this PR
chosen?


### Are there test cases added in this PR? _(If not, why?)_


### Provide a list of related PRs _(if any)_


### Command used to generate this PR:**_(Applicable only to SDK release
request PRs)_

### Checklists
- [ ] Added impacted package name to the issue description
- [ ] Does this PR needs any fixes in the SDK Generator?** _(If so,
create an Issue in the
[Autorest/typescript](https://github.com/Azure/autorest.typescript)
repository and link it here)_
- [ ] Added a changelog (if necessary)
2024-11-14 17:36:09 -05:00
..
review [eslint-plugin] add rule "@typescript-eslint/consistent-type-imports": "warn" 2024-10-30 15:48:52 +00:00
samples/v1-beta [EngSys] move to vendored version of cross-env via dev-tool 2024-11-02 00:48:06 +00:00
samples-dev Fix Communication JobRouter Sample Issues (#28517) 2024-02-15 08:45:49 -08:00
src [communication] Migrate @azure-rest/communication-job-router to ESM/vitest (#31774) 2024-11-14 17:36:09 -05:00
test [communication] Migrate @azure-rest/communication-job-router to ESM/vitest (#31774) 2024-11-14 17:36:09 -05:00
CHANGELOG.md Post release automated changes for communication releases (#29307) 2024-04-13 00:27:11 +00:00
README.md Small README updates (#27853) 2023-11-20 14:04:40 -08:00
api-extractor.json [communication] Migrate @azure-rest/communication-job-router to ESM/vitest (#31774) 2024-11-14 17:36:09 -05:00
assets.json [ACS JobRouter] Max concurrent offers on workers (#28490) 2024-03-06 00:58:08 +00:00
eslint.config.mjs Apply automated migration to eslint flat config 2024-08-22 11:43:19 -07:00
package.json [communication] Migrate @azure-rest/communication-job-router to ESM/vitest (#31774) 2024-11-14 17:36:09 -05:00
sample.env Job router ga samples and tests (#27654) 2023-11-09 23:30:59 -08:00
tsconfig.browser.config.json [communication] Migrate @azure-rest/communication-job-router to ESM/vitest (#31774) 2024-11-14 17:36:09 -05:00
tsconfig.json [communication] Migrate @azure-rest/communication-job-router to ESM/vitest (#31774) 2024-11-14 17:36:09 -05:00
tsp-location.yaml [ACS JobRouter] Max concurrent offers on workers (#28490) 2024-03-06 00:58:08 +00:00
vitest.browser.config.ts [communication] Migrate @azure-rest/communication-job-router to ESM/vitest (#31774) 2024-11-14 17:36:09 -05:00
vitest.config.ts [communication] Migrate @azure-rest/communication-job-router to ESM/vitest (#31774) 2024-11-14 17:36:09 -05:00

README.md

Azure Communication Services Job Router REST client library for JavaScript

This package contains a JavaScript SDK for Azure Communication Services Job Router. Read more about Azure Communication Services here

Please rely heavily on our REST client docs to use this library

Key links:

Getting started

Currently supported environments

  • LTS versions of Node.js

Prerequisites

Have an ACS Resource

Create an ACS resource in the Azure Portal or use an existing resource.

Install the @azure-rest/communication-job-router package

Install the Job Router REST client library for JavaScript with npm:

npm install @azure-rest/communication-job-router

Create and authenticate an AzureCommunicationRoutingServiceClient

To use an Azure Active Directory (AAD) token credential, provide an instance of the desired credential type obtained from the @azure/identity library.

To authenticate with AAD, you must first npm install @azure/identity

After setup, you can choose which type of credential from @azure/identity to use. As an example, DefaultAzureCredential can be used to authenticate the client.

Set the values of the client ID, tenant ID, and client secret of the AAD application as environment variables: AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_CLIENT_SECRET

Tutorial: Route jobs to workers using the Azure Communication Services Job Router SDK

In this tutorial, you will learn:

  • How to create a queue.
  • How to create workers and assign them to a queue.
  • How to route jobs to workers.
  • How to subscribe to and handle Job Router events.
  • How to complete and close jobs.

Start a NodeJS Express server

In a shell (cmd, PowerShell, Bash, etc.) create a folder called RouterQuickStart and inside this folder execute npx express-generator. This will generate a simple Express project that will listen on port 3000.

Example

mkdir RouterQuickStart
cd RouterQuickStart
npx express-generator
npm install
DEBUG=routerquickstart:* npm start

Install the Azure ACS Job Router SDK

In the RouterQuickStart folder, install the ACS Job Router SDK by executing npm install @azure-rest/communication-job-router --save.

Routing Jobs

Construct an AzureCommunicationRoutingServiceClient

First we need to construct an AzureCommunicationRoutingServiceClient.

const JobRouterClient = require("@azure-rest/communication-job-router").default;

const connectionString = "endpoint=https://<YOUR_ACS>.communication.azure.com/;accesskey=<YOUR_ACCESS_KEY>";
const routerClient = JobRouterClient(connectionString);

Create a Distribution Policy

This policy determines which workers will receive job offers as jobs are distributed off their queues.

const distributionPolicyId = "distribution-policy-id-1";
const distributionPolicy = await routerClient.path("/routing/distributionPolicies/{id}", distributionPolicyId).patch({
  contentType: "application/merge-patch+json",
  body: {
    name: "Default Distribution Policy",
    offerExpiresAfterSeconds: 30,
    mode: {
      kind: "longestIdle",
      minConcurrentOffers: 1,
      maxConcurrentOffers: 3,
    },
  }
});

Create a Queue

This queue offers jobs to workers according to our previously created distribution policy.

const salesQueueId = "sales-queue-id-1";
const salesQueue = await routerClient.path("/routing/queues/{id}", salesQueueId).patch({
  contentType: "application/merge-patch+json",
  body: {
    distributionPolicyId: distributionPolicyId,
    name: "Main",
    labels: {},
  }
});

Create Workers

These workers are assigned to our previously created "Sales" queue and have some labels.

  • setting availableForOffers to true means these workers are ready to accept job offers.
  • refer to our labels documentation to better understand labels and label selectors.
// Create worker "Alice".
const workerAliceId = "773accfb-476e-42f9-a202-b211b41a4ea4";
const workerAlice = await routerClient.path("/routing/workers/{id}", workerAliceId).patch({
  contentType: "application/merge-patch+json",
  body: {
    capacity: 120,
    queues: [salesQueueId],
    labels: {
      Xbox: 5,
      german: 4,
      name: "Alice"
    },
    channels: [
      {
        channelId: "CustomChatChannel",
        capacityCostPerJob: 10,
      },
      {
        channelId: "CustomVoiceChannel",
        capacityCostPerJob: 100,
      },
    ],
  }
});

// Create worker "Bob".
const workerBobId = "21837c88-6967-4078-86b9-1207821a8392";
const workerBob = await routerClient.path("/routing/workers/{id}", workerBobId).patch({
  contentType: "application/merge-patch+json",
  body: {
    capacity: 100,
    queues: [salesQueueId],
    labels: {
      Xbox: 5,
      english: 3,
      name: "Alice"
    },
    channels: [
      {
        channelId: "CustomChatChannel",
        capacityCostPerJob: 10,
      },
      {
        channelId: "CustomVoiceChannel",
        capacityCostPerJob: 100,
      },
    ],
  }
});

Job Lifecycle

Refer to our job lifecycle documentation to better understand the lifecycle of a job.

Create a Job

This job is enqueued on our previously created "Sales" queue.

const jobId = "router-job-123";
const result = await routerClient.path("/routing/jobs/{id}", jobId).patch({
  contentType: "application/merge-patch+json",
  body: {
    channelReference: "66e4362e-aad5-4d71-bb51-448672ebf492",
    channelId: "voice",
    priority: 2,
    queueId: salesQueueId,
    labels: {},
  }
});

(Optional) Create a Job With a Classification Policy

Create a Classification Policy

This policy classifies jobs upon creation.

const classificationPolicyId = "classification-policy-1";
const classificationPolicy = await routerClient.path("/routing/classificationPolicies/{id}", classificationPolicyId).patch({
  contentType: "application/merge-patch+json",
  body: {
    name: "Default Classification Policy",
    fallbackQueueId: salesQueueId,
    queueSelectorAttachments: [
      {
        kind: "static",
        queueSelector: { key: "department", labelOperator: "equal", value: "xbox" }
      },
    ],
    workerSelectorAttachments: [
      {
        kind: "static",
        workerSelector: { key: "english", labelOperator: "greaterThan", value: 5 }
      }
    ],
    prioritizationRule: {
      kind: "expression",
      language: "powerFx",
      expression: "If(job.department = \"xbox\", 2, 1)"
    }
  }
});

Create and classify a job

This job will be classified with our previously created classification policy. It also has a label.

const job = await routerClient.path("/routing/jobs/{id}", jobId).patch({
  contentType: "application/merge-patch+json",
  body: {
    channelReference: "66e4362e-aad5-4d71-bb51-448672ebf492",
    channelId: "voice",
    classificationPolicyId: classificationPolicy.body.id,
    labels: {
      department: "xbox"
    },
  }
});

Events

Job Router events are delivered via Azure Event Grid. Refer to our Azure Event Grid documentation to better understand Azure Event Grid.

In the previous example:

  • The job gets enqueued to the “Sales" queue.
  • A worker is selected to handle the job, a job offer is issued to that worker, and a RouterWorkerOfferIssued event is sent via Azure Event Grid.

Example RouterWorkerOfferIssued JSON shape:

{
  "id": "1027db4a-17fe-4a7f-ae67-276c3120a29f",
  "topic": "/subscriptions/{subscription-id}/resourceGroups/{group-name}/providers/Microsoft.Communication/communicationServices/{communication-services-resource-name}",
  "subject": "worker/{worker-id}/job/{job-id}",
  "data": {
    "workerId": "w100",
    "jobId": "7f1df17b-570b-4ae5-9cf5-fe6ff64cc712",
    "channelReference": "test-abc",
    "channelId": "FooVoiceChannelId",
    "queueId": "625fec06-ab81-4e60-b780-f364ed96ade1",
    "offerId": "525fec06-ab81-4e60-b780-f364ed96ade1",
    "offerTimeUtc": "2023-08-17T02:43:30.3847144Z",
    "expiryTimeUtc": "2023-08-17T02:44:30.3847674Z",
    "jobPriority": 5,
    "jobLabels": {
      "Locale": "en-us",
      "Segment": "Enterprise",
      "Token": "FooToken"
    },
    "jobTags": {
      "Locale": "en-us",
      "Segment": "Enterprise",
      "Token": "FooToken"
    }
  },
  "eventType": "Microsoft.Communication.RouterWorkerOfferIssued",
  "dataVersion": "1.0",
  "metadataVersion": "1",
  "eventTime": "2023-08-17T00:55:25.1736293Z"
}

Subscribing to Events

One way to subscribe to ACS Job Router events is through the Azure Portal.

  1. Navigate to your ACS resource in the Azure Portal and open the “Events” blade.
  2. Add an event subscription for the “RouterWorkerOfferIssued” event.
  3. Select an appropriate means to receive the event (e.g. Webhook, Azure Functions, Service Bus).

Refer to our "subscribe to Job Router events" documentation to better understand subscribing to Job Router events.

The route in your NodeJS application that receives events may look something like this:

app.post('/event', (req, res) => {
    req.body.forEach(eventGridEvent => {
        // Deserialize the event data into the appropriate type
        if (eventGridEvent.eventType === "Microsoft.EventGrid.SubscriptionValidationEvent") {
            res.send({ validationResponse: eventGridEvent.data.validationCode });
        } else if (eventGridEvent.eventType === "Microsoft.Azure.CommunicationServices.RouterWorkerOfferIssued") {
           // RouterWorkerOfferIssued handling logic;
        } else if ...
    });
    ...
});

Accept or Decline the Job Offer

Once you receive a RouterWorkerOfferIssued event you can accept or decline the job offer.

  • workerId - Id of the worker accepting or declining the job offer.
  • offerId - Id of the offer being accepted or declined.
const acceptResponse = await routerClient.path("/routing/workers/{workerId}/offers/{offerId}:accept", workerId, offerId).post();
// or
const declineResponse = await routerClient.path("/routing/workers/{workerId}/offers/{offerId}:decline", workerId, offerId).post();

Complete the Job

The assignmentId received from the previous step's response is required to complete the job. Completing the job puts it in the wrap-up phase of its lifecycle.

const completeJob = await routerClient.path("/routing/jobs/{jobId}/assignments/{assignmentId}:complete", jobId, acceptResponse.body.assignmentId).post({
  body: {
    note: `Job has been completed by ${workerId} at ${new Date()}`
  }
});

Close the Job

Once the worker has completed the wrap-up phase of the job we can close the job and attach a note to it for future reference.

const closeJob = await routerClient.path("/routing/jobs/{jobId}/assignments/{assignmentId}:close", jobId, acceptResponse.body.assignmentId).post({
  body: {
    note: `Job has been closed by ${workerId} at ${new Date()}`
  }
});

Troubleshooting

Logging

Enabling logging may help uncover useful information about failures. In order to see a log of HTTP requests and responses, set the AZURE_LOG_LEVEL environment variable to info. Alternatively, logging can be enabled at runtime by calling setLogLevel in the @azure/logger:

const { setLogLevel } = require("@azure/logger");

setLogLevel("info");

For more detailed instructions on how to enable logs, you can look at the @azure/logger package docs.