You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by po...@apache.org on 2020/12/27 13:52:55 UTC

[airflow-cancel-workflow-runs] 39/44: Adds 'allDuplicates' mode of cancelling (#10)

This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airflow-cancel-workflow-runs.git

commit 99869d37d982384d18c79539b67df94f17557cbe
Author: Jarek Potiuk <ja...@potiuk.com>
AuthorDate: Sat Oct 31 18:18:25 2020 +0100

    Adds 'allDuplicates' mode of cancelling (#10)
---
 README.md     |   81 +-
 action.yml    |   17 +-
 dist/index.js | 4723 ++++++++-------------------------------------------------
 src/main.ts   | 1325 +++++++++++-----
 4 files changed, 1673 insertions(+), 4473 deletions(-)

diff --git a/README.md b/README.md
index 0a13160..e81c66b 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,8 @@
 - [Inputs and outputs](#inputs-and-outputs)
   - [Inputs](#inputs)
   - [Outputs](#outputs)
-- [Examples](#examples)
+  - [Most often used canceling example](#most-often-used-canceling-example)
+- [More Examples](#more-examples)
   - [Repositories that use Pull Requests from forks](#repositories-that-use-pull-requests-from-forks)
     - [Cancel duplicate runs for the source workflow](#cancel-duplicate-runs-for-the-source-workflow)
     - [Cancel duplicate jobs for triggered workflow](#cancel-duplicate-jobs-for-triggered-workflow)
@@ -98,10 +99,6 @@ If you want a comprehensive solution, you should use the action as follows:
    The `workflow_run` should be responsible for all canceling actions. The examples below show
    the possible ways the action can be utilized.
 
-The previous version of this action (v1) used `schedule` events to cancel previous runs. With
-the announcement of pull_request_target by GitHub, the v2 version discourages using this pattern
-and `schedule` events are no longer needed.
-
 # Inputs and outputs
 
 ## Inputs
@@ -122,12 +119,13 @@ and `schedule` events are no longer needed.
 
 The job cancel modes work as follows:
 
-| Cancel Mode  | No `sourceRunId` specified                                             | The `sourceRunId` set to `${{ github.event.workflow_run.id }}`                      |
-|--------------|------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
-| `duplicates` | Cancels past, duplicate runs from the same repo/branch as current run. | Cancels past, duplicate runs for the same repo/branch as the source run.            |
-| `self`       | Cancels self run.                                                      | Cancel the `sourceRunId` run.                                                       |
-| `failedJobs` | Cancels all runs of own workflow that have matching jobs that failed.  | Cancels all runs of the `sourceRunId` workflow that have matching jobs that failed. |
-| `namedJobs`  | Cancels all runs of own workflow that have matching jobs.              | Cancels all runs of the `sourceRunId` workflow that have matching jobs.             |
+| Cancel Mode     | No `sourceRunId` specified                                             | The `sourceRunId` set to `${{ github.event.workflow_run.id }}`                      |
+|-----------------|------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
+| `duplicates`    | Cancels duplicate runs from the same repo/branch as current run.       | Cancels duplicate runs for the same repo/branch as the source run.                  |
+| `allDuplicates` | Cancels duplicate runs from all running workflows.                     | Cancels duplicate runs from all running workflows.                                  |
+| `self`          | Cancels self run.                                                      | Cancel the `sourceRunId` run.                                                       |
+| `failedJobs`    | Cancels all runs of own workflow that have matching jobs that failed.  | Cancels all runs of the `sourceRunId` workflow that have matching jobs that failed. |
+| `namedJobs`     | Cancels all runs of own workflow that have matching jobs.              | Cancels all runs of the `sourceRunId` workflow that have matching jobs.             |
 
 
 ## Outputs
@@ -143,7 +141,38 @@ The job cancel modes work as follows:
 | `sourceEvent`       | Current event: ``${{ github.event }}``                  | Event of the run that triggered this `workflow_run`                                                  |
 | `cancelledRuns`     | JSON-stringified array of cancelled run ids.            | JSON-stringified array of cancelled run ids.                                                         |
 
-# Examples
+## Most often used canceling example
+
+The most common canceling example is that you want to cancel all duplicates appearing in your build queue.
+As of 4.1 version of the Action this can be realised by single workflow run that can cancel all duplicates
+for all running workflows. It is resistant to temporary queues - as it can cancel also the future, queued
+workflows that have duplicated, fresher (also queued workflows and this is recommended for everyone.
+
+The below example is a "workflow_run" type of event. The workflow_run event always has "write" access that allows
+it to cancel other workflows - even if they are coming from pull request.
+
+```yaml
+name: Cancelling Duplicates
+on:
+  workflow_run:
+    workflows: ['CI']
+    types: ['requested']
+
+jobs:
+  cancel-duplicate-workflow-runs:
+    name: "Cancel duplicate workflow runs"
+    runs-on: ubuntu-latest
+    steps:
+      - uses: potiuk/cancel-workflow-runs@v4_1
+        name: "Cancel duplicate workflow runs"
+        with:
+          cancelMode: allDuplicates
+          token: ${{ secrets.GITHUB_TOKEN }}
+          sourceRunId: ${{ github.event.workflow_run.id }}
+```
+
+
+# More Examples
 
 Note that you can combine the steps below in several steps of the same job. The examples here are showing
 one step per case for clarity.
@@ -202,7 +231,7 @@ jobs:
     name: "Cancel duplicate workflow runs"
     runs-on: ubuntu-latest
     steps:
-      - uses: potiuk/cancel-workflow-runs@v2
+      - uses: potiuk/cancel-workflow-runs@v4_1
         name: "Cancel duplicate workflow runs"
         with:
           cancelMode: duplicates
@@ -263,7 +292,7 @@ jobs:
       sourceHeadSha: ${{ steps.cancel.outputs.sourceHeadSha }}
       sourceEvent: ${{ steps.cancel.outputs.sourceEvent }}
     steps:
-      - uses: potiuk/cancel-workflow-runs@v2
+      - uses: potiuk/cancel-workflow-runs@v4_1
         id: cancel
         name: "Cancel duplicate CI runs"
         with:
@@ -276,7 +305,7 @@ jobs:
             that you will not see in the list of checks.
 
             You can checks the status of those images in:
-      - uses: potiuk/cancel-workflow-runs@v2
+      - uses: potiuk/cancel-workflow-runs@v4_1
         name: "Cancel duplicate Cancelling runs"
         with:
           cancelMode: namedJobs
@@ -326,7 +355,7 @@ on:
     runs-on: ubuntu-latest
     steps:
       - name: "Cancel the self CI workflow run"
-        uses: potiuk/cancel-workflow-runs@v2
+        uses: potiuk/cancel-workflow-runs@v4_1
         with:
           cancelMode: self
           notifyPRCancel: true
@@ -355,7 +384,7 @@ on:
     runs-on: ubuntu-latest
     steps:
       - name: "Cancel the self Cancelling workflow run"
-        uses: potiuk/cancel-workflow-runs@v2
+        uses: potiuk/cancel-workflow-runs@v4_1
         with:
           cancelMode: self
           notifyPRCancel: true
@@ -388,7 +417,7 @@ jobs:
     name: "Fail fast CI runs"
     runs-on: ubuntu-latest
     steps:
-      - uses: potiuk/cancel-workflow-runs@v2
+      - uses: potiuk/cancel-workflow-runs@v4_1
         name: "Fail fast CI runs"
         with:
           cancelMode: failedJobs
@@ -429,7 +458,7 @@ jobs:
     name: "Fail fast CI runs"
     runs-on: ubuntu-latest
     steps:
-      - uses: potiuk/cancel-workflow-runs@v2
+      - uses: potiuk/cancel-workflow-runs@v4_1
         name: "Fail fast CI. Source run: ${{ github.event.workflow_run.id }}"
         id: cancel-failed
         with:
@@ -452,7 +481,7 @@ jobs:
             echo "::set-output name=matching-regexp::${REGEXP}"
       - name: "Cancel triggered 'Cancelling' runs for the cancelled failed runs"
         if: steps.cancel-failed.outputs.cancelledRuns != '[]'
-        uses: potiuk/cancel-workflow-runs@master
+        uses: potiuk/cancel-workflow-runs@v4_1
         with:
           cancelMode: namedJobs
           token: ${{ secrets.GITHUB_TOKEN }}
@@ -487,7 +516,7 @@ jobs:
     name: "Fail fast Canceling runs"
     runs-on: ubuntu-latest
     steps:
-      - uses: potiuk/cancel-workflow-runs@v2
+      - uses: potiuk/cancel-workflow-runs@v4_1
         name: "Fail fast Canceling runs"
         with:
           cancelMode: failedJobs
@@ -512,7 +541,7 @@ on:
     runs-on: ubuntu-latest
     steps:
       - name: "Cancel the self CI workflow run"
-        uses: potiuk/cancel-workflow-runs@v2
+        uses: potiuk/cancel-workflow-runs@v4_1
         with:
           cancelMode: duplicates
           cancelFutureDuplicates: true
@@ -553,7 +582,7 @@ jobs:
     name: "Cancel duplicate workflow runs"
     runs-on: ubuntu-latest
     steps:
-      - uses: potiuk/cancel-workflow-runs@v2
+      - uses: potiuk/cancel-workflow-runs@v4_1
         name: "Cancel duplicate workflow runs"
         with:
           cancelMode: duplicates
@@ -577,7 +606,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: "Cancel the self workflow run"
-        uses: potiuk/cancel-workflow-runs@v2
+        uses: potiuk/cancel-workflow-runs@v4_1
         with:
           cancelMode: self
           token: ${{ secrets.GITHUB_TOKEN }}
@@ -603,7 +632,7 @@ jobs:
     name: "Cancel failed runs"
     runs-on: ubuntu-latest
     steps:
-      - uses: potiuk/cancel-workflow-runs@v2
+      - uses: potiuk/cancel-workflow-runs@v4_1
         name: "Cancel failed runs"
         with:
           cancelMode: failedJobs
@@ -635,7 +664,7 @@ jobs:
     name: "Cancel the self workflow run"
     runs-on: ubuntu-latest
     steps:
-      - uses: potiuk/cancel-workflow-runs@v2
+      - uses: potiuk/cancel-workflow-runs@v4_1
         name: "Cancel past CI runs"
         with:
           cancelMode: namedJobs
diff --git a/action.yml b/action.yml
index aaec312..541094a 100644
--- a/action.yml
+++ b/action.yml
@@ -29,12 +29,17 @@ inputs:
   cancelMode:
     description: |
       The mode of cancel. One of:
-          * `duplicates`  - cancels past, duplicate runs from the same repo/branch as local run or
-                            sourceId workflow. This is the default mode when cancelMode is not specified.
-          * `self`        - cancels self run - either own run if sourceRunId is not set, or
-                            the source run that triggered the `workflow_run'
-          * `failedJobs`  - cancels all runs that failed in jobs matching one of the regexps
-          * `namedJobs`   - cancels runs where names of some jobs match some of regexps
+          * `duplicates`    - cancels duplicate runs from the same repo/branch as local run or
+                              sourceId workflow. This is the default mode when cancelMode is not specified.
+          * `allDuplicates` - cancels duplicate runs from all workflows. It is more aggressive version of
+                              duplicate canceling - as it cancels all duplicates. It is helpful in case
+                              of long queues of builds - as it is enough that one of the workflows that
+                              cancel duplicates is executed, it can effectively clean-up the queue in this
+                              case for all the future, queued runs.
+          * `self`          - cancels self run - either own run if sourceRunId is not set, or
+                              the source run that triggered the `workflow_run'
+          * `failedJobs`    - cancels all runs that failed in jobs matching one of the regexps
+          * `namedJobs`     - cancels runs where names of some jobs match some of regexps
     required: false
   cancelFutureDuplicates:
     description: |
diff --git a/dist/index.js b/dist/index.js
index 69bd164..6f047aa 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -1465,76 +1465,138 @@ var __importStar = (this && this.__importStar) || function (mod) {
 Object.defineProperty(exports, "__esModule", { value: true });
 const github = __importStar(__webpack_require__(469));
 const core = __importStar(__webpack_require__(393));
-const treemap = __importStar(__webpack_require__(706));
-const CANCELLABLE_RUNS = [
+/**
+ Those are the cancellable event types tht we know about
+ */
+const CANCELLABLE_EVENT_TYPES = [
     'push',
     'pull_request',
     'workflow_run',
     'schedule',
     'workflow_dispatch'
 ];
+/**
+ * Those are different modes for cancelling
+ */
 var CancelMode;
 (function (CancelMode) {
     CancelMode["DUPLICATES"] = "duplicates";
+    CancelMode["ALL_DUPLICATES"] = "allDuplicates";
     CancelMode["SELF"] = "self";
     CancelMode["FAILED_JOBS"] = "failedJobs";
     CancelMode["NAMED_JOBS"] = "namedJobs";
 })(CancelMode || (CancelMode = {}));
-function createListRunsQueryOtherRuns(octokit, owner, repo, status, workflowId, headBranch, eventName) {
+/**
+ * Converts the source of a run object into a string that can be used as map key in maps where we keep
+ * arrays of runs per source group
+ * @param sourceGroup the object identifying the source of the run (Pull Request, Master Push)
+ * @returns the unique string id for the source group
+ */
+function getSourceGroupId(sourceGroup) {
+    return `:${sourceGroup.workflowId}:${sourceGroup.headRepo}:${sourceGroup.headBranch}:${sourceGroup.eventName}`;
+}
+/**
+ * Creates query parameters selecting all runs that share the same source group as we have. This can
+ * be used to select duplicates of my own run.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param mySourceGroup - source group of the originating run
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
+function createListRunsQueryRunsSameSource(repositoryInfo, status, mySourceGroup) {
     const request = {
-        owner,
-        repo,
+        owner: repositoryInfo.owner,
+        repo: repositoryInfo.repo,
         // eslint-disable-next-line @typescript-eslint/camelcase
-        workflow_id: workflowId,
+        workflow_id: mySourceGroup.workflowId,
         status,
-        branch: headBranch,
-        event: eventName
+        branch: mySourceGroup.headBranch,
+        event: mySourceGroup.eventName
     };
-    return octokit.actions.listWorkflowRuns.endpoint.merge(request);
+    return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request);
 }
-function createListRunsQueryMyOwnRun(octokit, owner, repo, status, workflowId, runId) {
+/**
+ * Creates query parameters selecting only specific run Id.
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param workflowId - Id of the workflow to retrieve
+ * @param runId - run Id to retrieve
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
+function createListRunsQuerySpecificRunId(repositoryInfo, status, workflowId, runId) {
     const request = {
-        owner,
-        repo,
+        owner: repositoryInfo.owner,
+        repo: repositoryInfo.repo,
         // eslint-disable-next-line @typescript-eslint/camelcase
         workflow_id: workflowId,
         status,
         // eslint-disable-next-line @typescript-eslint/camelcase
         run_id: runId.toString()
     };
-    return octokit.actions.listWorkflowRuns.endpoint.merge(request);
+    return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request);
 }
-function createListRunsQueryAllRuns(octokit, owner, repo, status, workflowId) {
+/**
+ * Creates query parameters selecting all run Ids for specified workflow Id.
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param workflowId - Id of the workflow to retrieve
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
+function createListRunsQueryAllRuns(repositoryInfo, status, workflowId) {
     const request = {
-        owner,
-        repo,
+        owner: repositoryInfo.owner,
+        repo: repositoryInfo.repo,
         // eslint-disable-next-line @typescript-eslint/camelcase
         workflow_id: workflowId,
         status
     };
-    return octokit.actions.listWorkflowRuns.endpoint.merge(request);
+    return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request);
 }
-function createJobsForWorkflowRunQuery(octokit, owner, repo, runId) {
+/**
+ * Creates query parameters selecting all jobs for specified run Id.
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @return query parameters merged with the listJobsForWorkflowRun criteria
+ */
+function createJobsForWorkflowRunQuery(repositoryInfo, runId) {
     const request = {
-        owner,
-        repo,
+        owner: repositoryInfo.owner,
+        repo: repositoryInfo.repo,
         // eslint-disable-next-line @typescript-eslint/camelcase
         run_id: runId
     };
-    return octokit.actions.listJobsForWorkflowRun.endpoint.merge(request);
+    return repositoryInfo.octokit.actions.listJobsForWorkflowRun.endpoint.merge(request);
 }
-function matchInArray(s, regexps) {
+/**
+ * Returns true if the string matches any of the regexps in array of regexps
+ * @param stringToMatch string to match
+ * @param regexps array of regexp to match the string against
+ * @return true if there is a match
+ */
+function matchInArray(stringToMatch, regexps) {
     for (const regexp of regexps) {
-        if (s.match(regexp)) {
+        if (stringToMatch.match(regexp)) {
             return true;
         }
     }
     return false;
 }
-function jobsMatchingNames(octokit, owner, repo, runId, jobNameRegexps, checkIfFailed) {
+/**
+ * Returns true if the runId specified has jobs matching the regexp and optionally checks if those
+ * jobs are failed.
+ * * If checkIfFailed is False, it returns true if any of the job name for the run match any of the regexps
+ * * Id checkIfFailed is True, it returns true if any of the matching jobs have status 'failed'
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @param jobNameRegexps - array of job name regexps
+ * @param checkIfFailed - whether to check the 'failed' status of matched jobs
+ * @return true if there is a match
+ */
+function jobsMatchingNames(repositoryInfo, runId, jobNameRegexps, checkIfFailed) {
     var e_1, _a;
     return __awaiter(this, void 0, void 0, function* () {
-        const listJobs = createJobsForWorkflowRunQuery(octokit, owner, repo, runId);
+        const listJobs = createJobsForWorkflowRunQuery(repositoryInfo, runId);
         if (checkIfFailed) {
             core.info(`\nChecking if runId ${runId} has job names matching any of the ${jobNameRegexps} that failed\n`);
         }
@@ -1542,7 +1604,7 @@ function jobsMatchingNames(octokit, owner, repo, runId, jobNameRegexps, checkIfF
             core.info(`\nChecking if runId ${runId} has job names matching any of the ${jobNameRegexps}\n`);
         }
         try {
-            for (var _b = __asyncValues(octokit.paginate.iterator(listJobs)), _c; _c = yield _b.next(), !_c.done;) {
+            for (var _b = __asyncValues(repositoryInfo.octokit.paginate.iterator(listJobs)), _c; _c = yield _b.next(), !_c.done;) {
                 const item = _c.value;
                 for (const job of item.data.jobs) {
                     core.info(`    The job name: ${job.name}, Conclusion: ${job.conclusion}`);
@@ -1550,11 +1612,13 @@ function jobsMatchingNames(octokit, owner, repo, runId, jobNameRegexps, checkIfF
                         if (checkIfFailed) {
                             // Only fail the build if one of the matching jobs fail
                             if (job.conclusion === 'failure') {
-                                core.info(`    The Job ${job.name} matches one of the ${jobNameRegexps} regexps and it failed. Cancelling run.`);
+                                core.info(`    The Job ${job.name} matches one of the ${jobNameRegexps} regexps and it failed.` +
+                                    ` Cancelling run.`);
                                 return true;
                             }
                             else {
-                                core.info(`    The Job ${job.name} matches one of the ${jobNameRegexps} regexps but it did not fail. So far, so good.`);
+                                core.info(`    The Job ${job.name} matches one of the ${jobNameRegexps} regexps but it did not fail. ` +
+                                    ` So far, so good.`);
                             }
                         }
                         else {
@@ -1576,30 +1640,52 @@ function jobsMatchingNames(octokit, owner, repo, runId, jobNameRegexps, checkIfF
         return false;
     });
 }
-function getWorkflowId(octokit, runId, owner, repo) {
+/**
+ * Retrieves workflowId from the workflow URL.
+ * @param workflowUrl workflow URL to retrieve the ID from
+ * @return numerical workflow id
+ */
+function retrieveWorkflowIdFromUrl(workflowUrl) {
+    const workflowIdString = workflowUrl.split('/').pop() || '';
+    if (!(workflowIdString.length > 0)) {
+        throw new Error('Could not resolve workflow');
+    }
+    return parseInt(workflowIdString);
+}
+/**
+ * Returns workflowId of the runId specified
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @return workflow ID for the run id
+ */
+function getWorkflowId(repositoryInfo, runId) {
     return __awaiter(this, void 0, void 0, function* () {
-        const reply = yield octokit.actions.getWorkflowRun({
-            owner,
-            repo,
+        const reply = yield repositoryInfo.octokit.actions.getWorkflowRun({
+            owner: repositoryInfo.owner,
+            repo: repositoryInfo.repo,
             // eslint-disable-next-line @typescript-eslint/camelcase
             run_id: runId
         });
         core.info(`The source run ${runId} is in ${reply.data.workflow_url} workflow`);
-        const workflowIdString = reply.data.workflow_url.split('/').pop() || '';
-        if (!(workflowIdString.length > 0)) {
-            throw new Error('Could not resolve workflow');
-        }
-        return parseInt(workflowIdString);
+        return retrieveWorkflowIdFromUrl(reply.data.workflow_url);
     });
 }
-function getWorkflowRuns(octokit, statusValues, cancelMode, createListRunQuery) {
+/**
+ * Returns workflow runs matching the callable adding query criteria
+ * @param repositoryInfo - information about the repository used
+ * @param statusValues - array of string status values for runs that we are interested at
+ * @param cancelMode - which cancel mode the query is about
+ * @param createListRunQuery - what is the callable criteria selection
+ * @return map of workflow run items indexed by the workflow run number
+ */
+function getWorkflowRuns(repositoryInfo, statusValues, cancelMode, createListRunQuery) {
     var e_2, _a;
     return __awaiter(this, void 0, void 0, function* () {
-        const workflowRuns = new treemap.TreeMap();
+        const workflowRuns = new Map();
         for (const status of statusValues) {
             const listRuns = yield createListRunQuery(status);
             try {
-                for (var _b = __asyncValues(octokit.paginate.iterator(listRuns)), _c; _c = yield _b.next(), !_c.done;) {
+                for (var _b = __asyncValues(repositoryInfo.octokit.paginate.iterator(listRuns)), _c; _c = yield _b.next(), !_c.done;) {
                     const item = _c.value;
                     // There is some sort of bug where the pagination URLs point to a
                     // different endpoint URL which trips up the resulting representation
@@ -1622,14 +1708,114 @@ function getWorkflowRuns(octokit, statusValues, cancelMode, createListRunQuery)
         return workflowRuns;
     });
 }
-function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNamesRegexps, skipEventTypes) {
+/**
+ * True if the request is candidate for cancelling in case of duplicate deletion
+ * @param runItem item to check
+ * @param headRepo head Repo that we are checking against
+ * @param cancelFutureDuplicates whether future duplicates are being cancelled
+ * @param sourceRunId the source Run Id that originates the request
+ * @return true if we determine that the run Id should be cancelled
+ */
+function isCandidateForCancellingDuplicate(runItem, headRepo, cancelFutureDuplicates, sourceRunId) {
+    const runHeadRepo = runItem.head_repository.full_name;
+    if (headRepo !== undefined && runHeadRepo !== headRepo) {
+        core.info(`\nThe run ${runItem.id} is from a different ` +
+            `repo: ${runHeadRepo} (expected ${headRepo}). Not cancelling it\n`);
+        return false;
+    }
+    if (cancelFutureDuplicates) {
+        core.info(`\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`);
+        return true;
+    }
+    else {
+        if (runItem.id === sourceRunId) {
+            core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`);
+            return false;
+        }
+        else if (runItem.id > sourceRunId) {
+            core.info(`\nThe run ${runItem.id} is started later than my own run ${sourceRunId}. Not returning it\n`);
+            return false;
+        }
+        core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`);
+        return true;
+    }
+}
+/**
+ * Should the run is candidate for cancelling in SELF cancelling mode?
+ * @param runItem run item
+ * @param sourceRunId source run id
+ * @return true if the run item is self
+ */
+function isCandidateForCancellingSelf(runItem, sourceRunId) {
+    if (runItem.id === sourceRunId) {
+        core.info(`\nReturning the "source" run: ${runItem.id}.\n`);
+        return true;
+    }
+    else {
+        return false;
+    }
+}
+/**
+ * Should the run is candidate for cancelling in naming job cancelling mode?
+ * @param repositoryInfo - information about the repository used
+ * @param runItem run item
+ * @param jobNamesRegexps array of regexps to match job names against
+ * @return true if the run item contains jobs with names matching the pattern
+ */
+function isCandidateForCancellingNamedJobs(repositoryInfo, runItem, jobNamesRegexps) {
+    return __awaiter(this, void 0, void 0, function* () {
+        // Cancel all jobs that have failed jobs (no matter when started)
+        if (yield jobsMatchingNames(repositoryInfo, runItem.id, jobNamesRegexps, false)) {
+            core.info(`\nSome jobs have matching names in ${runItem.id} . Returning it.\n`);
+            return true;
+        }
+        else {
+            core.info(`\nNone of the jobs match name in ${runItem.id}. Returning it.\n`);
+            return false;
+        }
+    });
+}
+/**
+ * Should the run is candidate for cancelling in failed job cancelling mode?
+ * @param repositoryInfo - information about the repository used
+ * @param runItem run item
+ * @param jobNamesRegexps array of regexps to match job names against
+ * @return true if the run item contains failed jobs with names matching the pattern
+ */
+function isCandidateForCancellingFailedJobs(repositoryInfo, runItem, jobNamesRegexps) {
+    return __awaiter(this, void 0, void 0, function* () {
+        // Cancel all jobs that have failed jobs (no matter when started)
+        if (yield jobsMatchingNames(repositoryInfo, runItem.id, jobNamesRegexps, true)) {
+            core.info(`\nSome matching named jobs failed in ${runItem.id} . Cancelling it.\n`);
+            return true;
+        }
+        else {
+            core.info(`\nNone of the matching jobs failed in ${runItem.id}. Not cancelling it.\n`);
+            return false;
+        }
+    });
+}
+/**
+ * Determines whether the run is candidate to be cancelled depending on the mode used
+ * @param repositoryInfo - information about the repository used
+ * @param runItem - run item
+ * @param headRepo - head repository
+ * @param cancelMode - cancel mode
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param sourceRunId - what is the source run id
+ * @param jobNamesRegexps - what are the regexps for job names
+ * @param skipEventTypes - which events should be skipped
+ * @return true if the run id is candidate for cancelling
+ */
+function isCandidateForCancelling(repositoryInfo, runItem, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNamesRegexps, skipEventTypes) {
     return __awaiter(this, void 0, void 0, function* () {
         if ('completed' === runItem.status.toString()) {
             core.info(`\nThe run ${runItem.id} is completed. Not cancelling it.\n`);
             return false;
         }
-        if (!CANCELLABLE_RUNS.includes(runItem.event.toString())) {
-            core.info(`\nThe run ${runItem.id} is (${runItem.event} event - not in ${CANCELLABLE_RUNS}). Not cancelling it.\n`);
+        if (!CANCELLABLE_EVENT_TYPES.includes(runItem.event.toString())) {
+            core.info(`\nThe run ${runItem.id} is (${runItem.event} event - not ` +
+                `in ${CANCELLABLE_EVENT_TYPES}). Not cancelling it.\n`);
             return false;
         }
         if (skipEventTypes.includes(runItem.event.toString())) {
@@ -1638,72 +1824,38 @@ function shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode,
             return false;
         }
         if (cancelMode === CancelMode.FAILED_JOBS) {
-            // Cancel all jobs that have failed jobs (no matter when started)
-            if (yield jobsMatchingNames(octokit, owner, repo, runItem.id, jobNamesRegexps, true)) {
-                core.info(`\nSome matching named jobs failed in ${runItem.id} . Cancelling it.\n`);
-                return true;
-            }
-            else {
-                core.info(`\nNone of the matching jobs failed in ${runItem.id}. Not cancelling it.\n`);
-                return false;
-            }
+            return yield isCandidateForCancellingFailedJobs(repositoryInfo, runItem, jobNamesRegexps);
         }
         else if (cancelMode === CancelMode.NAMED_JOBS) {
-            // Cancel all jobs that have failed jobs (no matter when started)
-            if (yield jobsMatchingNames(octokit, owner, repo, runItem.id, jobNamesRegexps, false)) {
-                core.info(`\nSome jobs have matching names in ${runItem.id} . Returning it.\n`);
-                return true;
-            }
-            else {
-                core.info(`\nNone of the jobs match name in ${runItem.id}. Returning it.\n`);
-                return false;
-            }
+            return yield isCandidateForCancellingNamedJobs(repositoryInfo, runItem, jobNamesRegexps);
         }
         else if (cancelMode === CancelMode.SELF) {
-            if (runItem.id === sourceRunId) {
-                core.info(`\nReturning the "source" run: ${runItem.id}.\n`);
-                return true;
-            }
-            else {
-                return false;
-            }
+            return isCandidateForCancellingSelf(runItem, sourceRunId);
         }
         else if (cancelMode === CancelMode.DUPLICATES) {
-            const runHeadRepo = runItem.head_repository.full_name;
-            if (headRepo !== undefined && runHeadRepo !== headRepo) {
-                core.info(`\nThe run ${runItem.id} is from a different ` +
-                    `repo: ${runHeadRepo} (expected ${headRepo}). Not cancelling it\n`);
-                return false;
-            }
-            if (cancelFutureDuplicates) {
-                core.info(`\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`);
-                return true;
-            }
-            else {
-                if (runItem.id === sourceRunId) {
-                    core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`);
-                    return false;
-                }
-                else if (runItem.id > sourceRunId) {
-                    core.info(`\nThe run ${runItem.id} is started later than my own run ${sourceRunId}. Not returning it\n`);
-                    return false;
-                }
-                core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`);
-                return true;
-            }
+            return isCandidateForCancellingDuplicate(runItem, headRepo, cancelFutureDuplicates, sourceRunId);
+        }
+        else if (cancelMode === CancelMode.ALL_DUPLICATES) {
+            core.info(`Returning candidate ${runItem.id} for "all_duplicates" cancelling.`);
+            return true;
         }
         else {
             throw Error(`\nWrong cancel mode ${cancelMode}! This should never happen.\n`);
         }
     });
 }
-function cancelRun(octokit, owner, repo, runId) {
+/**
+ * Cancels the specified workflow run.
+ * @param repositoryInfo - information about the repository used
+ * @param runId - run Id to cancel
+ */
+function cancelRun(repositoryInfo, runId) {
     return __awaiter(this, void 0, void 0, function* () {
         let reply;
         try {
-            reply = yield octokit.actions.cancelWorkflowRun({
-                owner,
-                repo,
+            reply = yield repositoryInfo.octokit.actions.cancelWorkflowRun({
+                owner: repositoryInfo.owner,
+                repo: repositoryInfo.repo,
                 // eslint-disable-next-line @typescript-eslint/camelcase
                 run_id: runId
             });
@@ -1714,122 +1866,288 @@ function cancelRun(octokit, owner, repo, runId) {
         }
     });
 }
-function findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason) {
+/**
+ * Returns map of workflow run items matching the criteria specified group by workflow run id
+ * @param repositoryInfo - information about the repository used
+ * @param statusValues - status values we want to check
+ * @param cancelMode - cancel mode to use
+ * @param sourceWorkflowId - source workflow id
+ * @param sourceRunId - source run id
+ * @param sourceEventName - name of the source event
+ * @param mySourceGroup - source of the run that originated it
+ * @return map of the run items matching grouped by workflow run id
+ */
+function getWorkflowRunsMatchingCriteria(repositoryInfo, statusValues, cancelMode, sourceWorkflowId, sourceRunId, sourceEventName, mySourceGroup) {
     return __awaiter(this, void 0, void 0, function* () {
-        const statusValues = ['queued', 'in_progress'];
-        const workflowRuns = yield getWorkflowRuns(octokit, statusValues, cancelMode, function (status) {
+        return yield getWorkflowRuns(repositoryInfo, statusValues, cancelMode, function (status) {
             if (cancelMode === CancelMode.SELF) {
-                core.info(`\nFinding runs for my own run: Owner: ${owner}, Repo: ${repo}, ` +
+                core.info(`\nFinding runs for my own run: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
                     `Workflow ID:${sourceWorkflowId}, Source Run id: ${sourceRunId}\n`);
-                return createListRunsQueryMyOwnRun(octokit, owner, repo, status, sourceWorkflowId, sourceRunId);
+                return createListRunsQuerySpecificRunId(repositoryInfo, status, sourceWorkflowId, sourceRunId);
             }
             else if (cancelMode === CancelMode.FAILED_JOBS ||
-                cancelMode === CancelMode.NAMED_JOBS) {
-                core.info(`\nFinding runs for all runs: Owner: ${owner}, Repo: ${repo}, Status: ${status} ` +
-                    `Workflow ID:${sourceWorkflowId}\n`);
-                return createListRunsQueryAllRuns(octokit, owner, repo, status, sourceWorkflowId);
+                cancelMode === CancelMode.NAMED_JOBS ||
+                cancelMode === CancelMode.ALL_DUPLICATES) {
+                core.info(`\nFinding runs for all runs: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
+                    `Status: ${status} Workflow ID:${sourceWorkflowId}\n`);
+                return createListRunsQueryAllRuns(repositoryInfo, status, sourceWorkflowId);
             }
             else if (cancelMode === CancelMode.DUPLICATES) {
-                core.info(`\nFinding duplicate runs: Owner: ${owner}, Repo: ${repo}, Status: ${status} ` +
-                    `Workflow ID:${sourceWorkflowId}, Head Branch: ${headBranch},` +
+                core.info(`\nFinding duplicate runs: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
+                    `Status: ${status} Workflow ID:${sourceWorkflowId}, Head Branch: ${mySourceGroup.headBranch},` +
                     `Event name: ${sourceEventName}\n`);
-                return createListRunsQueryOtherRuns(octokit, owner, repo, status, sourceWorkflowId, headBranch, sourceEventName);
+                return createListRunsQueryRunsSameSource(repositoryInfo, status, mySourceGroup);
             }
             else {
                 throw Error(`\nWrong cancel mode ${cancelMode}! This should never happen.\n`);
             }
         });
-        const workflowsToCancel = [];
-        const pullRequestToNotify = [];
+    });
+}
+/**
+ * Finds pull request matching its headRepo, headBranch and headSha
+ * @param repositoryInfo - information about the repository used
+ * @param headRepo - head repository from which Pull Request comes
+ * @param headBranch - head branch from which Pull Request comes
+ * @param headSha - sha for the head of the incoming Pull request
+ */
+function findPullRequest(repositoryInfo, headRepo, headBranch, headSha) {
+    return __awaiter(this, void 0, void 0, function* () {
+        // Finds Pull request for this workflow run
+        core.info(`\nFinding PR request id for: owner: ${repositoryInfo.owner}, Repo:${repositoryInfo.repo},` +
+            ` Head:${headRepo}:${headBranch}.\n`);
+        const pullRequests = yield repositoryInfo.octokit.pulls.list({
+            owner: repositoryInfo.owner,
+            repo: repositoryInfo.repo,
+            head: `${headRepo}:${headBranch}`
+        });
+        for (const pullRequest of pullRequests.data) {
+            core.info(`\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`);
+            if (pullRequest.head.sha === headSha) {
+                core.info(`\nFound PR: ${pullRequest.number}\n`);
+                return pullRequest;
+            }
+        }
+        core.info(`\nCould not find the PR for this build :(\n`);
+        return null;
+    });
+}
+/**
+ * Finds pull request id for the run item.
+ * @param repositoryInfo - information about the repository used
+ * @param runItem - run Item that the pull request should be found for
+ * @return pull request number to notify (or undefined if not found)
+ */
+function findPullRequestForRunItem(repositoryInfo, runItem) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const pullRequest = yield findPullRequest(repositoryInfo, runItem.head_repository.owner.login, runItem.head_branch, runItem.head_sha);
+        if (pullRequest) {
+            return pullRequest.number;
+        }
+        return undefined;
+    });
+}
+/**
+ * Maps found workflow runs into groups - filters out the workflows that are not eligible for canceling
+ * (depends on cancel Mode) and assigns each workflow to groups - where workflow runs from the
+ * same group are put together in one array - in a map indexed by the source group id.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param headRepo - head repository the event comes from
+ * @param cancelMode - cancel mode to use
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param sourceRunId - source run id for the run
+ * @param jobNameRegexps - regexps for job names
+ * @param skipEventTypes - array of event names to skip
+ * @param workflowRuns - map of workflow runs found
+ * @parm map where key is the source group id and value is array of workflow run item candidates to cancel
+ */
+function filterAndMapWorkflowRunsToGroups(repositoryInfo, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNameRegexps, skipEventTypes, workflowRuns) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const mapOfWorkflowRunCandidates = new Map();
         for (const [key, runItem] of workflowRuns) {
             core.info(`\nChecking run number: ${key}, RunId: ${runItem.id}, Url: ${runItem.url}. Status ${runItem.status},` +
                 ` Created at ${runItem.created_at}\n`);
-            if (yield shouldBeCancelled(octokit, owner, repo, runItem, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNameRegexps, skipEventTypes)) {
-                if (notifyPRCancel && runItem.event === 'pull_request') {
-                    const pullRequest = yield findPullRequest(octokit, owner, repo, runItem.head_repository.owner.login, runItem.head_branch, runItem.head_sha);
-                    if (pullRequest) {
-                        pullRequestToNotify.push(pullRequest.number);
-                    }
+            if (yield isCandidateForCancelling(repositoryInfo, runItem, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNameRegexps, skipEventTypes)) {
+                const candidateSourceGroup = {
+                    workflowId: retrieveWorkflowIdFromUrl(runItem.workflow_url),
+                    headBranch: runItem.head_branch,
+                    headRepo: runItem.head_repository.full_name,
+                    eventName: runItem.event
+                };
+                const sourceGroupId = getSourceGroupId(candidateSourceGroup);
+                let workflowRunArray = mapOfWorkflowRunCandidates.get(sourceGroupId);
+                if (workflowRunArray === undefined) {
+                    workflowRunArray = [];
+                    mapOfWorkflowRunCandidates.set(sourceGroupId, workflowRunArray);
                 }
-                workflowsToCancel.push([runItem.id, runItem.created_at]);
-            }
-        }
-        // Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
-        const sortedRunTuplesToCancel = workflowsToCancel.sort((runTuple1, runTuple2) => runTuple2[1].localeCompare(runTuple1[1]));
-        if (sortedRunTuplesToCancel.length > 0) {
-            if (cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) {
-                core.info(`\nSkipping the first run (${sortedRunTuplesToCancel[0]}) of all the matching ` +
-                    `duplicates - this one we are going to leave in peace!\n`);
-                sortedRunTuplesToCancel.shift();
-            }
-            if (sortedRunTuplesToCancel.length === 0) {
-                core.info(`\nNo duplicates to cancel!\n`);
-                return sortedRunTuplesToCancel.map(runTuple => runTuple[0]);
-            }
-            core.info('\n######  Cancelling runs starting from the oldest  ##########\n' +
-                `\n     Runs to cancel: ${sortedRunTuplesToCancel.length}\n` +
-                `\n     PRs to notify: ${pullRequestToNotify.length}\n`);
-            for (const runTuple of sortedRunTuplesToCancel) {
-                core.info(`\nCancelling run: ${runTuple}.\n`);
-                yield cancelRun(octokit, owner, repo, runTuple[0]);
-            }
-            for (const pullRequestNumber of pullRequestToNotify) {
-                const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`;
-                yield addCommentToPullRequest(octokit, owner, repo, pullRequestNumber, `[The Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`);
+                core.info(`The candidate ${runItem.id} has been added to ${sourceGroupId} group of candidates`);
+                workflowRunArray.push(runItem);
             }
-            core.info('\n######  Finished cancelling runs                  ##########\n');
         }
-        else {
-            core.info('\n######  There are no runs to cancel!              ##########\n');
-        }
-        return sortedRunTuplesToCancel.map(runTuple => runTuple[0]);
+        return mapOfWorkflowRunCandidates;
     });
 }
-function getRequiredEnv(key) {
-    const value = process.env[key];
-    if (value === undefined) {
-        const message = `${key} was not defined.`;
-        throw new Error(message);
-    }
-    return value;
-}
-function addCommentToPullRequest(octokit, owner, repo, pullRequestNumber, comment) {
+/**
+ * Add specified comment to Pull Request
+ * @param repositoryInfo - information about the repository used
+ * @param pullRequestNumber - number of pull request
+ * @param comment - comment to add
+ */
+function addCommentToPullRequest(repositoryInfo, pullRequestNumber, comment) {
     return __awaiter(this, void 0, void 0, function* () {
         core.info(`\nNotifying PR: ${pullRequestNumber} with '${comment}'.\n`);
-        yield octokit.issues.createComment({
-            owner,
-            repo,
+        yield repositoryInfo.octokit.issues.createComment({
+            owner: repositoryInfo.owner,
+            repo: repositoryInfo.repo,
             // eslint-disable-next-line @typescript-eslint/camelcase
             issue_number: pullRequestNumber,
             body: comment
         });
     });
 }
-function findPullRequest(octokit, owner, repo, headRepo, headBranch, headSha) {
+/**
+ * Notifies PR about cancelling
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - my own run id
+ * @param pullRequestNumber - number of pull request
+ * @param reason reason for canceling
+ */
+function notifyPR(repositoryInfo, selfRunId, pullRequestNumber, reason) {
     return __awaiter(this, void 0, void 0, function* () {
-        // Finds Pull request for this workflow run
-        core.info(`\nFinding PR request id for: owner: ${owner}, Repo:${repo}, Head:${headRepo}:${headBranch}.\n`);
-        const pullRequests = yield octokit.pulls.list({
-            owner,
-            repo,
-            head: `${headRepo}:${headBranch}`
-        });
-        for (const pullRequest of pullRequests.data) {
-            core.info(`\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`);
-            if (pullRequest.head.sha === headSha) {
-                core.info(`\nFound PR: ${pullRequest.number}\n`);
-                return pullRequest;
+        const selfWorkflowRunUrl = `https://github.com/${repositoryInfo.owner}/${repositoryInfo.repo}` +
+            `/actions/runs/${selfRunId}`;
+        yield addCommentToPullRequest(repositoryInfo, pullRequestNumber, `[The Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`);
+    });
+}
+/**
+ * Cancels all runs in the specified group of runs.
+ * @param repositoryInfo - information about the repository used
+ * @param sortedRunItems - items sorted in descending order (descending order by created_at)
+ * @param notifyPRCancel - whether to notify the PR when cancelling
+ * @param selfRunId - what is the self run id
+ * @param sourceGroupId - what is the source group id
+ * @param reason - reason for canceling
+ */
+function cancelAllRunsInTheSourceGroup(repositoryInfo, sortedRunItems, notifyPRCancel, selfRunId, sourceGroupId, reason) {
+    return __awaiter(this, void 0, void 0, function* () {
+        core.info(`\n###### Cancelling runs for ${sourceGroupId} starting from the most recent  ##########\n` +
+            `\n     Number of runs to cancel: ${sortedRunItems.length}\n`);
+        const cancelledRuns = [];
+        for (const runItem of sortedRunItems) {
+            if (notifyPRCancel && runItem.event === 'pull_request') {
+                const pullRequestNumber = yield findPullRequestForRunItem(repositoryInfo, runItem);
+                if (pullRequestNumber !== undefined) {
+                    core.info(`\nNotifying PR: ${pullRequestNumber} (runItem: ${runItem}) with: ${reason}\n`);
+                    yield notifyPR(repositoryInfo, selfRunId, pullRequestNumber, reason);
+                }
             }
+            core.info(`\nCancelling run: ${runItem}.\n`);
+            yield cancelRun(repositoryInfo, runItem.id);
+            cancelledRuns.push(runItem.id);
         }
-        core.info(`\nCould not find the PR for this build :(\n`);
-        return null;
+        core.info(`\n######  Finished cancelling runs for ${sourceGroupId} ##########\n`);
+        return cancelledRuns;
     });
 }
-function getOrigin(octokit, runId, owner, repo) {
+/**
+ * Cancels found runs in a smart way. It takes all the found runs group by the source group, sorts them
+ * descending according to create date in each of the source groups and cancels them in that order -
+ * optionally skipping the first found run in each source group in case of duplicates.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param mapOfWorkflowRunsCandidatesToCancel map of all workflow run candidates
+ * @param cancelMode - cancel mode
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param notifyPRCancel - whether to notify PRs with comments
+ * @param selfRunId - self run Id
+ * @param reason - reason for canceling
+ */
+function cancelTheRunsPerGroup(repositoryInfo, mapOfWorkflowRunsCandidatesToCancel, cancelMode, cancelFutureDuplicates, notifyPRCancel, selfRunId, reason) {
     return __awaiter(this, void 0, void 0, function* () {
-        const reply = yield octokit.actions.getWorkflowRun({
-            owner,
-            repo,
+        const cancelledRuns = [];
+        for (const [sourceGroupId, candidatesArray] of mapOfWorkflowRunsCandidatesToCancel) {
+            // Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
+            const sortedRunItems = candidatesArray.sort((runItem1, runItem2) => runItem2.created_at.localeCompare(runItem1.created_at));
+            if (sortedRunItems.length > 0) {
+                if ((cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) ||
+                    cancelMode === CancelMode.ALL_DUPLICATES) {
+                    core.info(`\nSkipping the first run (${sortedRunItems[0].id}) of all the matching ` +
+                        `duplicates for '${sourceGroupId}'. This one we are going to leave in peace!\n`);
+                    sortedRunItems.shift();
+                }
+                if (sortedRunItems.length === 0) {
+                    core.info(`\nNo duplicates to cancel for ${sourceGroupId}!\n`);
+                    continue;
+                }
+                cancelledRuns.push(...(yield cancelAllRunsInTheSourceGroup(repositoryInfo, sortedRunItems, notifyPRCancel, selfRunId, sourceGroupId, reason)));
+            }
+            else {
+                core.info(`\n######  There are no runs to cancel for ${sourceGroupId} ##########\n`);
+            }
+        }
+        return cancelledRuns;
+    });
+}
+/**
+ * Find and cancels runs based on the criteria chosen.
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - number of own run id
+ * @param sourceWorkflowId - source workflow id that triggered the workflow
+ *        (might be different than self for workflow_run)
+ * @param sourceRunId - source run id that triggered the workflow
+ *        (might be different than self for workflow_run)
+ * @param headRepo - head repository that triggered the workflow (repo from which PR came)
+ * @param headBranch - branch of the PR that triggered the workflow (when it is triggered by PR)
+ * @param sourceEventName - name of the event that triggered the workflow
+ *        (different than self event name for workflow_run)
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether to cancel future duplicates for duplicate cancelling
+ * @param notifyPRCancel - whether to notify in PRs about cancelling
+ * @param notifyPRMessageStart - whether to notify PRs when the action starts
+ * @param jobNameRegexps - array of regular expressions to match hob names in case of named modes
+ * @param skipEventTypes - array of event names that should be skipped
+ * @param reason - reason for cancelling
+ * @return array of canceled workflow run ids
+ */
+function findAndCancelRuns(repositoryInfo, selfRunId, sourceWorkflowId, sourceRunId, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const statusValues = ['queued', 'in_progress'];
+        const mySourceGroup = {
+            headBranch,
+            headRepo,
+            eventName: sourceEventName,
+            workflowId: sourceWorkflowId
+        };
+        const workflowRuns = yield getWorkflowRunsMatchingCriteria(repositoryInfo, statusValues, cancelMode, sourceWorkflowId, sourceRunId, sourceEventName, mySourceGroup);
+        const mapOfWorkflowRunsCandidatesToCancel = yield filterAndMapWorkflowRunsToGroups(repositoryInfo, headRepo, cancelMode, cancelFutureDuplicates, sourceRunId, jobNameRegexps, skipEventTypes, workflowRuns);
+        return yield cancelTheRunsPerGroup(repositoryInfo, mapOfWorkflowRunsCandidatesToCancel, cancelMode, cancelFutureDuplicates, notifyPRCancel, selfRunId, reason);
+    });
+}
+/**
+ * Returns environment variable that is required - throws error if it is not defined.
+ * @param key key for the env variable
+ * @return value of the env variable
+ */
+function getRequiredEnv(key) {
+    const value = process.env[key];
+    if (value === undefined) {
+        const message = `${key} was not defined.`;
+        throw new Error(message);
+    }
+    return value;
+}
+/**
+ * Gets origin of the runId - if this is a workflow run, it returns the information about the source run
+ * @param repositoryInfo - information about the repository used
+ * @param runId - run id of the run to check
+ * @return information about the triggering workflow
+ */
+function getOrigin(repositoryInfo, runId) {
+    return __awaiter(this, void 0, void 0, function* () {
+        const reply = yield repositoryInfo.octokit.actions.getWorkflowRun({
+            owner: repositoryInfo.owner,
+            repo: repositoryInfo.repo,
             // eslint-disable-next-line @typescript-eslint/camelcase
             run_id: runId
         });
@@ -1839,22 +2157,44 @@ function getOrigin(octokit, runId, owner, repo) {
             `Event: ${sourceRun.event}, Head sha: ${sourceRun.head_sha}, url: ${sourceRun.url}`);
         let pullRequest = null;
         if (sourceRun.event === 'pull_request') {
-            pullRequest = yield findPullRequest(octokit, owner, repo, sourceRun.head_repository.owner.login, sourceRun.head_branch, sourceRun.head_sha);
-        }
-        return [
-            reply.data.head_repository.full_name,
-            reply.data.head_branch,
-            reply.data.event,
-            reply.data.head_sha,
-            pullRequest ? pullRequest.merge_commit_sha : '',
-            pullRequest
-        ];
+            pullRequest = yield findPullRequest(repositoryInfo, sourceRun.head_repository.owner.login, sourceRun.head_branch, sourceRun.head_sha);
+        }
+        return {
+            headRepo: reply.data.head_repository.full_name,
+            headBranch: reply.data.head_branch,
+            headSha: reply.data.head_sha,
+            mergeCommitSha: pullRequest ? pullRequest.merge_commit_sha : null,
+            pullRequest: pullRequest ? pullRequest : null,
+            eventName: reply.data.event
+        };
     });
 }
-function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates) {
+/**
+ * Performs the actual cancelling.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - number of own run id
+ * @param sourceWorkflowId - source workflow id that triggered the workflow
+ *        (might be different than self for workflow_run)
+ * @param sourceRunId - source run id that triggered the workflow
+ *        (might be different than self for workflow_run)
+ * @param headRepo - head repository that triggered the workflow (repo from which PR came)
+ * @param headBranch - branch of the PR that triggered the workflow (when it is triggered by PR)
+ * @param sourceEventName - name of the event that triggered the workflow
+ *        (different than self event name for workflow_run)
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether to cancel future duplicates for duplicate cancelling
+ * @param notifyPRCancel - whether to notify in PRs about cancelling
+ * @param notifyPRCancelMessage - message to send when cancelling the PR (overrides default message
+ *        generated automatically)
+ * @param notifyPRMessageStart - whether to notify PRs when the action starts
+ * @param jobNameRegexps - array of regular expressions to match hob names in case of named modes
+ * @param skipEventTypes - array of event names that should be skipped
+ */
+function performCancelJob(repositoryInfo, selfRunId, sourceWorkflowId, sourceRunId, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates) {
     return __awaiter(this, void 0, void 0, function* () {
         core.info('\n###################################################################################\n');
-        core.info(`All parameters: owner: ${owner}, repo: ${repo}, run id: ${sourceRunId}, ` +
+        core.info(`All parameters: owner: ${repositoryInfo.owner}, repo: ${repositoryInfo.repo}, run id: ${sourceRunId}, ` +
             `head repo ${headRepo}, headBranch: ${headBranch}, ` +
             `sourceEventName: ${sourceEventName}, cancelMode: ${cancelMode}, jobNames: ${jobNameRegexps}`);
         core.info('\n###################################################################################\n');
@@ -1874,20 +2214,139 @@ function performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, own
             reason = `It has jobs matching ${jobNameRegexps}.`;
         }
         else if (cancelMode === CancelMode.DUPLICATES) {
-            core.info(`# Cancel duplicate runs started before ${sourceRunId} for workflow ${sourceWorkflowId}.`);
-            reason = `It in earlier duplicate of ${sourceWorkflowId} run.`;
+            core.info(`# Cancel duplicate runs for workflow ${sourceWorkflowId} for same triggering branch as own run Id.`);
+            reason = `It is an earlier duplicate of ${sourceWorkflowId} run.`;
+        }
+        else if (cancelMode === CancelMode.ALL_DUPLICATES) {
+            core.info(`# Cancel all duplicates runs started for workflow ${sourceWorkflowId}.`);
+            reason = `It is an earlier duplicate of ${sourceWorkflowId} run.`;
         }
         else {
             throw Error(`Wrong cancel mode ${cancelMode}! This should never happen.`);
         }
         core.info('\n###################################################################################\n');
-        return yield findAndCancelRuns(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason);
+        return yield findAndCancelRuns(repositoryInfo, selfRunId, sourceWorkflowId, sourceRunId, headRepo, headBranch, sourceEventName, cancelMode, cancelFutureDuplicates, notifyPRCancel, notifyPRMessageStart, jobNameRegexps, skipEventTypes, reason);
     });
 }
+/**
+ * Retrieves information about source workflow Id. Either from the current event or from the workflow
+ * nme specified. If the file name is not specified, the workflow to act on is either set to self event
+ * or in case of workflow_run event - to the workflow id that triggered the 'workflow_run' event.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param workflowFileName - optional workflow file name
+ * @param sourceRunId - source run id of the workfow
+ * @param selfRunId - self run id
+ * @return workflow id that is associate with the workflow we are going to act on.
+ */
+function retrieveWorkflowId(repositoryInfo, workflowFileName, sourceRunId, selfRunId) {
+    return __awaiter(this, void 0, void 0, function* () {
+        let sourceWorkflowId;
+        if (workflowFileName) {
+            sourceWorkflowId = workflowFileName;
+            core.info(`\nFinding runs for another workflow found by ${workflowFileName} name: ${sourceWorkflowId}\n`);
+        }
+        else {
+            sourceWorkflowId = yield getWorkflowId(repositoryInfo, sourceRunId);
+            if (sourceRunId === selfRunId) {
+                core.info(`\nFinding runs for my own workflow ${sourceWorkflowId}\n`);
+            }
+            else {
+                core.info(`\nFinding runs for source workflow ${sourceWorkflowId}\n`);
+            }
+        }
+        return sourceWorkflowId;
+    });
+}
+/**
+ * Sets output but also prints the output value in the logs.
+ *
+ * @param name name of the output
+ * @param value value of the output
+ */
 function verboseOutput(name, value) {
     core.info(`Setting output: ${name}: ${value}`);
     core.setOutput(name, value);
 }
+/**
+ * Performs sanity check of the parameters passed. Some of the parameter combinations do not work so they
+ * are verified and in case od unexpected combination found, approrpriate error is raised.
+ *
+ * @param eventName - name of the event to act on
+ * @param sourceRunId - run id of the triggering event
+ * @param selfRunId - our own run id
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether future duplicate cancelling is enabled
+ * @param jobNameRegexps - array of regular expression of job names
+ */
+function performSanityChecks(eventName, sourceRunId, selfRunId, cancelMode, cancelFutureDuplicates, jobNameRegexps) {
+    if (eventName === 'workflow_run' &&
+        sourceRunId === selfRunId &&
+        cancelMode === CancelMode.DUPLICATES) {
+        throw Error(`You cannot run "workflow_run" in ${cancelMode} cancelMode without "sourceId" input.` +
+            'It will likely not work as you intended - it will cancel runs which are not duplicates!' +
+            'See the docs for details.');
+    }
+    if (jobNameRegexps.length > 0 &&
+        [
+            CancelMode.DUPLICATES,
+            CancelMode.SELF,
+            CancelMode.ALL_DUPLICATES
+        ].includes(cancelMode)) {
+        throw Error(`You cannot specify jobNames on ${cancelMode} cancelMode.`);
+    }
+    if (cancelMode === CancelMode.ALL_DUPLICATES && !cancelFutureDuplicates) {
+        throw Error(`The  ${cancelMode} cancelMode has to have cancelFutureDuplicates set to true.`);
+    }
+}
+/**
+ * Produces basic outputs for the action. This does not include cancelled workflow run id - those are
+ * set after cancelling is done.
+ *
+ * @param triggeringWorkflowInfo
+ */
+function produceBasicOutputs(triggeringWorkflowInfo) {
+    verboseOutput('sourceHeadRepo', triggeringWorkflowInfo.headRepo);
+    verboseOutput('sourceHeadBranch', triggeringWorkflowInfo.headBranch);
+    verboseOutput('sourceHeadSha', triggeringWorkflowInfo.headSha);
+    verboseOutput('sourceEvent', triggeringWorkflowInfo.eventName);
+    verboseOutput('pullRequestNumber', triggeringWorkflowInfo.pullRequest
+        ? triggeringWorkflowInfo.pullRequest.number.toString()
+        : '');
+    verboseOutput('mergeCommitSha', triggeringWorkflowInfo.mergeCommitSha
+        ? triggeringWorkflowInfo.mergeCommitSha
+        : '');
+    verboseOutput('targetCommitSha', triggeringWorkflowInfo.mergeCommitSha
+        ? triggeringWorkflowInfo.mergeCommitSha
+        : triggeringWorkflowInfo.headSha);
+}
+/**
+ * Notifies the PR that the action has started.
+ *
+ * @param repositoryInfo information about the repository
+ * @param triggeringWorkflowInfo information about the triggering workflow
+ * @param sourceRunId run id of the source workflow
+ * @param selfRunId self run id
+ * @param notifyPRMessageStart whether to notify about the start of the action
+ */
+function notifyActionStart(repositoryInfo, triggeringWorkflowInfo, sourceRunId, selfRunId, notifyPRMessageStart) {
+    return __awaiter(this, void 0, void 0, function* () {
+        if (notifyPRMessageStart && triggeringWorkflowInfo.pullRequest) {
+            const selfWorkflowRunUrl = `https://github.com/${repositoryInfo.owner}/${repositoryInfo.repo}` +
+                `/actions/runs/${selfRunId}`;
+            yield repositoryInfo.octokit.issues.createComment({
+                owner: repositoryInfo.owner,
+                repo: repositoryInfo.repo,
+                // eslint-disable-next-line @typescript-eslint/camelcase
+                issue_number: triggeringWorkflowInfo.pullRequest.number,
+                body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
+            });
+        }
+    });
+}
+/**
+ * Main run method that does everything :)
+ */
 function run() {
     return __awaiter(this, void 0, void 0, function* () {
         const token = core.getInput('token', { required: true });
@@ -1911,55 +2370,23 @@ function run() {
             : [];
         const workflowFileName = core.getInput('workflowFileName');
         const [owner, repo] = repository.split('/');
+        const repositoryInfo = {
+            octokit,
+            owner,
+            repo
+        };
         core.info(`\nGetting workflow id for source run id: ${sourceRunId}, owner: ${owner}, repo: ${repo},` +
             ` skipEventTypes: ${skipEventTypes}\n`);
-        let sourceWorkflowId;
-        if (workflowFileName) {
-            sourceWorkflowId = workflowFileName;
-            core.info(`\nFinding runs for another workflow found by ${workflowFileName} name: ${sourceWorkflowId}\n`);
-        }
-        else {
-            sourceWorkflowId = yield getWorkflowId(octokit, sourceRunId, owner, repo);
-            if (sourceRunId === selfRunId) {
-                core.info(`\nFinding runs for my own workflow ${sourceWorkflowId}\n`);
-            }
-            else {
-                core.info(`\nFinding runs for source workflow ${sourceWorkflowId}\n`);
-            }
-            if (eventName === 'workflow_run' && sourceRunId === selfRunId) {
-                if (cancelMode === CancelMode.DUPLICATES)
-                    throw Error(`You cannot run "workflow_run" in ${cancelMode} cancelMode without "sourceId" input.` +
-                        'It will likely not work as you intended - it will cancel runs which are not duplicates!' +
-                        'See the docs for details.');
-            }
-        }
+        const sourceWorkflowId = yield retrieveWorkflowId(repositoryInfo, workflowFileName, sourceRunId, selfRunId);
+        performSanityChecks(eventName, sourceRunId, selfRunId, cancelMode, cancelFutureDuplicates, jobNameRegexps);
         core.info(`Repository: ${repository}, Owner: ${owner}, Repo: ${repo}, ` +
             `Event name: ${eventName}, CancelMode: ${cancelMode}, ` +
             `sourceWorkflowId: ${sourceWorkflowId}, sourceRunId: ${sourceRunId}, selfRunId: ${selfRunId}, ` +
             `jobNames: ${jobNameRegexps}`);
-        if (jobNameRegexps.length > 0 &&
-            [CancelMode.DUPLICATES, CancelMode.SELF].includes(cancelMode)) {
-            throw Error(`You cannot specify jobNames on ${cancelMode} cancelMode.`);
-        }
-        const [headRepo, headBranch, sourceEventName, headSha, mergeCommitSha, pullRequest] = yield getOrigin(octokit, sourceRunId, owner, repo);
-        verboseOutput('sourceHeadRepo', headRepo);
-        verboseOutput('sourceHeadBranch', headBranch);
-        verboseOutput('sourceHeadSha', headSha);
-        verboseOutput('sourceEvent', sourceEventName);
-        verboseOutput('pullRequestNumber', pullRequest ? pullRequest.number.toString() : '');
-        verboseOutput('mergeCommitSha', mergeCommitSha);
-        verboseOutput('targetCommitSha', pullRequest ? mergeCommitSha : headSha);
-        const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`;
-        if (notifyPRMessageStart && pullRequest) {
-            yield octokit.issues.createComment({
-                owner,
-                repo,
-                // eslint-disable-next-line @typescript-eslint/camelcase
-                issue_number: pullRequest.number,
-                body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
-            });
-        }
-        const cancelledRuns = yield performCancelJob(octokit, selfRunId, sourceWorkflowId, sourceRunId, owner, repo, headRepo, headBranch, sourceEventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates);
+        const triggeringWorkflowInfo = yield getOrigin(repositoryInfo, sourceRunId);
+        produceBasicOutputs(triggeringWorkflowInfo);
+        yield notifyActionStart(repositoryInfo, triggeringWorkflowInfo, sourceRunId, selfRunId, notifyPRMessageStart);
+        const cancelledRuns = yield performCancelJob(repositoryInfo, selfRunId, sourceWorkflowId, sourceRunId, triggeringWorkflowInfo.headRepo, triggeringWorkflowInfo.headBranch, triggeringWorkflowInfo.eventName, cancelMode, notifyPRCancel, notifyPRCancelMessage, notifyPRMessageStart, jobNameRegexps, skipEventTypes, cancelFutureDuplicates);
         verboseOutput('cancelledRuns', JSON.stringify(cancelledRuns));
     });
 }
@@ -9118,3836 +9545,6 @@ module.exports = (promise, onFinally) => {
 
 /***/ }),
 
-/***/ 706:
-/***/ (function(module) {
-
-(function webpackUniversalModuleDefinition(root, factory) {
-	if(true)
-		module.exports = factory();
-	else { var i, a; }
-})(typeof self !== 'undefined' ? self : this, function() {
-return /******/ (function(modules) { // webpackBootstrap
-/******/ 	// The module cache
-/******/ 	var installedModules = {};
-/******/
-/******/ 	// The require function
-/******/ 	function __webpack_require__(moduleId) {
-/******/
-/******/ 		// Check if module is in cache
-/******/ 		if(installedModules[moduleId]) {
-/******/ 			return installedModules[moduleId].exports;
-/******/ 		}
-/******/ 		// Create a new module (and put it into the cache)
-/******/ 		var module = installedModules[moduleId] = {
-/******/ 			i: moduleId,
-/******/ 			l: false,
-/******/ 			exports: {}
-/******/ 		};
-/******/
-/******/ 		// Execute the module function
-/******/ 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
-/******/
-/******/ 		// Flag the module as loaded
-/******/ 		module.l = true;
-/******/
-/******/ 		// Return the exports of the module
-/******/ 		return module.exports;
-/******/ 	}
-/******/
-/******/
-/******/ 	// expose the modules object (__webpack_modules__)
-/******/ 	__webpack_require__.m = modules;
-/******/
-/******/ 	// expose the module cache
-/******/ 	__webpack_require__.c = installedModules;
-/******/
-/******/ 	// define getter function for harmony exports
-/******/ 	__webpack_require__.d = function(exports, name, getter) {
-/******/ 		if(!__webpack_require__.o(exports, name)) {
-/******/ 			Object.defineProperty(exports, name, {
-/******/ 				configurable: false,
-/******/ 				enumerable: true,
-/******/ 				get: getter
-/******/ 			});
-/******/ 		}
-/******/ 	};
-/******/
-/******/ 	// getDefaultExport function for compatibility with non-harmony modules
-/******/ 	__webpack_require__.n = function(module) {
-/******/ 		var getter = module && module.__esModule ?
-/******/ 			function getDefault() { return module['default']; } :
-/******/ 			function getModuleExports() { return module; };
-/******/ 		__webpack_require__.d(getter, 'a', getter);
-/******/ 		return getter;
-/******/ 	};
-/******/
-/******/ 	// Object.prototype.hasOwnProperty.call
-/******/ 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
-/******/
-/******/ 	// __webpack_public_path__
-/******/ 	__webpack_require__.p = "";
-/******/
-/******/ 	// Load entry module and return exports
-/******/ 	return __webpack_require__(__webpack_require__.s = 5);
-/******/ })
-/************************************************************************/
-/******/ ([
-/* 0 */
-/***/ (function(module, exports, __nested_webpack_require_2850__) {
-
-"use strict";
-
-
-/**
- * @private
- */
-const RED = 1;
-/**
- * @private
- */
-const BLACK = 2;
-
-/**
- * @private
- * A node for a red-black tree
- */
-class TreeNode {
-
-    /**
-     * Default constructor
-     */
-    constructor() {
-        /** left child */
-        this.left = null;
-        /** right child */
-        this.right = null;
-        /** parent node */
-        this.parent = null;
-        /** key object (additional 'value' data member is added in map-like classes) */
-        this.key = null;
-        /** by default new node is red */
-        this.color = RED;
-    }
-
-    /**
-     * @returns parent of parent
-     */
-    grandparent() {
-        let p = this.parent;
-        if (p === null) {
-            return null;
-        } // No parent means no grandparent
-        return p.parent;
-    }
-
-    /**
-     * @returns the other child of the same parent
-     */
-    sibling() {
-        let p = this.parent;
-        if (p === null) {
-            return null;
-        } // No parent means no sibling
-        if (this === p.left) {
-            return p.right;
-        }
-        else {
-            return p.left;
-        }
-    }
-
-    /**
-     * @returns another child of the grandparent
-     */
-    uncle() {
-        let p = this.parent;
-        if (p === null) {
-            return null;
-        } // No parent means no uncle
-        let g = p.parent;
-        if (g === null) {
-            return null;
-        } // No grandparent means no uncle
-        return p.sibling();
-    }
-}
-
-module.exports = {
-    TreeNode: TreeNode,
-    BLACK: BLACK,
-    RED: RED
-};
-
-/***/ }),
-/* 1 */
-/***/ (function(module, exports) {
-
-/**
- * Used by sets
- * @private
- */
-class KeyOnlyPolicy {
-    /**
-     * Returns key data from the specified node
-     * @param {*} n
-     */
-    fetch(n) {
-        return n.key;
-    }
-
-    /**
-     * Copies key data from one node to another
-     * @param {*} dst
-     * @param {*} src
-     */
-    copy(dst, src) {
-        dst.key = src.key;
-    }
-
-    /**
-     * @returns string representation of the key
-     * @param {*} node
-     */
-    toString(node) {
-        return String(node.key);
-    }
-}
-
-/**
- * Used by maps
- * @private
- */
-class KeyValuePolicy {
-    /**
-     * Returns key-value data from the specified node
-     * @param {*} n
-     */
-    fetch(n) {
-        return [n.key, n.value];
-    }
-
-    /**
-     * Copies key-value data from one node to another
-     * @param {*} dst
-     * @param {*} src
-     */
-    copy(dst, src) {
-        dst.key = src.key;
-        dst.value = src.value;
-    }
-
-    /**
-     * @returns string representation of key-value pair
-     * @param {*} node
-     */
-    toString(node) {
-        return String(node.key) + ':' + String(node.value);
-    }
-}
-
-/**
- * Used for iteration through values of a map
- * @private
- */
-class ValueOnlyPolicy {
-    /**
-     * Returns data from the specified node
-     * @param {*} n
-     */
-    fetch(n) {
-        return n.value;
-    }
-
-    /**
-     * Copies value data from one node to another
-     * @param {*} dst
-     * @param {*} src
-     */
-    copy(dst, src) {
-        dst.value = src.value;
-    }
-
-    /**
-     * @returns string representation of node's value
-     * @param {*} node
-     */
-    toString(node) {
-        return String(node.value);
-    }
-}
-
-module.exports = {
-    KeyOnlyPolicy: KeyOnlyPolicy,
-    ValueOnlyPolicy: ValueOnlyPolicy,
-    KeyValuePolicy: KeyValuePolicy
-};
-
-/***/ }),
-/* 2 */
-/***/ (function(module, exports, __nested_webpack_require_6477__) {
-
-"use strict";
-
-
-/** @ignore */
-const {TreeNode, RED, BLACK} = __nested_webpack_require_6477__(0);
-/** @ignore */
-const {JsIterator, JsReverseIterator} = __nested_webpack_require_6477__(3);
-/** @ignore */
-const {Iterator, ReverseIterator} = __nested_webpack_require_6477__(4);
-/** @ignore */
-const {KeyOnlyPolicy, ValueOnlyPolicy, KeyValuePolicy} = __nested_webpack_require_6477__(1);
-/** @ignore */
-const {InsertionResult} = __nested_webpack_require_6477__(8);
-
-/** insertion mode of a multimap, nodes with the same keys can be added */
-const INSERT_MULTI = 1;
-/** if a node with the same key already exists then the subsequent attempts are ignored */
-const INSERT_UNIQUE = 2;
-/** if a node with the same key already exists then it's value is replaced on subsequent attempts */
-const INSERT_REPLACE = 3;
-
-/**
- * @private
- * Special node in a tree is created for performance reasons
- */
-class Head {
-    /** default constructor */
-    constructor() {
-        /** node with the smallest key */
-        this.leftmost = this;
-        /** node with the largest key */
-        this.rightmost = this;
-        /** root node of the tree */
-        this.root = this;
-        /** number of nodes in the tree */
-        this.size = 0;
-        /** extra tag used in debuggin of unit tests */
-        this.id = 'HEAD';
-    }
-}
-
-/**
- * @private
- * 3-way comparison, similar to strcmp and memcp in C programming language
- * @returns +1 if the value of rhs is greater than lhs
- *          -1 if the value of rhs is less than lhs
- *           0 if values are the same
- */
-function compare(lhs, rhs) {
-    if (lhs < rhs) {
-        return -1;
-    }
-    else if (lhs === rhs) {
-        return 0;
-    }
-    else {
-        return 1;
-    }
-}
-
-/**
- * Red-black tree
- * @access private
- */
-class Tree {
-    /** default constructor of an empty tree */
-    constructor() {
-        /** head */
-        this.head = new Head();
-        /** 3-way comparison function */
-        this.compare = compare;
-        /** must be an instance of KeyOnlyPolicy for sets, or KeyValuePolicy for maps */
-        this.valuePolicy = new KeyOnlyPolicy();
-    }
-
-    /**
-     * Deletes all nodes in the tree
-     */
-    clear() {
-        this.head = new Head();
-    }
-
-    /**
-     * @returns number of nodes in the tree
-     */
-    size() {
-        return this.head.size;
-    }
-
-    /**
-     * @private
-     * A wrapper that calls 3-way comparison of node keys
-     * @param {*} lhs
-     * @param {*} rhs
-     */
-    compareNodes(lhs, rhs) {
-        return this.compare(lhs.key, rhs.key);
-    }
-
-    /**
-     * @private
-     * used by rotation operations
-     */
-    replaceNode(oldNode, newNode) {
-        if (oldNode === newNode) {
-            return;
-        }
-        if (oldNode.parent === null) {
-            this.head.root = newNode;
-        }
-        else {
-            if (oldNode === oldNode.parent.left) {
-                oldNode.parent.left = newNode;
-            }
-            else {
-                oldNode.parent.right = newNode;
-            }
-        }
-
-        if (!this.isLeaf(newNode)) {
-            newNode.parent = oldNode.parent;
-        }
-    }
-
-    /**
-     * Rebalances tree as described below
-
-              X                                           Y
-             / \                                         / \
-            Y   c         right rotate -->              a   X
-           / \            <--  left rotate                 / \
-          a   b                                           b   c
-     * @private
-     */
-    rotateLeft(node) {
-        let right = node.right;
-        if (this.isLeaf(right)) {
-            throw new Error('rotateLeft can\'t be performed. The tree is corrupted');
-        }
-        this.replaceNode(node, right);
-
-        node.right = right.left;
-        if (right.left !== null) {
-            right.left.parent = node;
-        }
-
-        right.left = node;
-        node.parent = right;
-    }
-
-    /**
-     * Rebalances tree as described in rotateLeft
-     * @param {*} node - parent node
-     */
-    rotateRight(node) {
-        let left = node.left;
-        if (this.isLeaf(left)) {
-            throw new Error('rotateRight can\'t be performed. The tree is corrupted');
-        }
-        this.replaceNode(node, left);
-
-        node.left = left.right;
-        if (left.right !== null) {
-            left.right.parent = node;
-        }
-
-        left.right = node;
-        node.parent = left;
-    }
-
-    /**
-     * @returns true - for null pointers and head node; false - for all other nodes
-     * @param {*} node
-     */
-    isLeaf(node) {
-        if (node === null || node === this.head) {
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Leaf nodes are considered 'black'. All real nodes contain 'color' data member
-     * @param {*} node
-     */
-    fetchColor(node) {
-        if (this.isLeaf(node)) {
-            return BLACK;
-        }
-        else {
-            return node.color;
-        }
-    }
-
-    /**
-     * Tests a node for 'blackness'.
-     * @param {*} node
-     */
-    isBlack(node) {
-        return (this.fetchColor(node) === BLACK);
-    }
-
-    /**
-     * Tests node for 'redness'.
-     * @param {*} node
-     */
-    isRed(node) {
-        return (this.fetchColor(node) === RED);
-    }
-
-    /* ===========================
-       INSERT
-       =========================== */
-    /**
-     * A node will be inserted into the tree even if nodes with the same key already exist
-     * @param {*} node
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     */
-    insertMulti(node) {
-        return this.insertNode(node, INSERT_MULTI);
-    }
-
-    /**
-     * The node is inserted into the tree only if nodes with the same key do not exist there
-     * @param {*} node
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     */
-    insertUnique(node) {
-        return this.insertNode(node, INSERT_UNIQUE);
-    }
-
-    /**
-     * The node is inserted. If a node with the same key exists it's value will be replaced by the value of the new node
-     * @param {*} node
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     */
-    insertOrReplace(node) {
-        return this.insertNode(node, INSERT_REPLACE);
-    }
-
-    /**
-     * @private
-     * Inserts node. Updates head node. Rebalances tree.
-     * @param {*} n - node
-     * @param {*} mode - one of INSERT_MULTI, INSERT_UNIQUE, INSERT_REPLACE
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     */
-    insertNode(n, mode = INSERT_MULTI) {
-        let res = this.insertNodeInternal(this.head.root, n, mode);
-        if (res.wasAdded) {
-            if (this.head.size === 0) {
-                this.head.root = n;
-                this.head.leftmost = n;
-                this.head.rightmost = n;
-
-                n.left = this.head;
-                n.right = this.head;
-            }
-            else if (this.head.leftmost.left === n) {
-                this.head.leftmost = n;
-                n.left = this.head;
-            }
-            else if (this.head.rightmost.right === n) {
-                this.head.rightmost = n;
-                n.right = this.head;
-            }
-            this.insertRepairTree(n);
-            this.head.size = this.head.size + 1;
-        }
-        return res;
-    }
-
-    /**
-     * @private
-     * Inserts node according to the mode
-     * @param {*} root - root node of the tree
-     * @param {*} n - node to be inserted
-     * @param {*} mode - one of INSERT_MULTI, INSERT_UNIQUE, INSERT_REPLACE
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     */
-    insertNodeInternal(root, n, mode) {
-        // recursively descend the tree until a leaf is found
-        let x = root;
-        let y = null;
-        let rc = -1;
-        // find matching node
-        while (!this.isLeaf(x)) {
-            y = x;
-            rc = this.compareNodes(n, y);
-            if (rc < 0) {
-                x = y.left;
-            }
-            else if (rc > 0) {
-                x = y.right;
-            }
-            else {
-                // node with the same key value
-                switch (mode) {
-                    case INSERT_UNIQUE:
-                        // it's a duplicate
-                        return new InsertionResult(false, false, undefined);
-                    case INSERT_REPLACE:
-                        this.valuePolicy.copy(y, n);
-                        return new InsertionResult(false, true, new Iterator(y, this));
-                    default:
-                        // INSERT_MULTI
-                        x = y.right;
-                }
-            }
-        }
-        if (this.isLeaf(y)) {
-            n.parent = null;
-            n.left = this.head;
-            n.right = this.head;
-        }
-        else {
-            n.parent = y;
-            if (rc < 0) {
-                y.left = n;
-            }
-            else {
-                y.right = n;
-            }
-        }
-        return new InsertionResult(true, false, new Iterator(n, this));
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
-     * @param {*} n - node
-     */
-    insertRepairTree(n) {
-        if (n.parent === null) {
-            this.repairCase1(n);
-        }
-        else if (this.isBlack(n.parent)) {
-        /* insert_case2(n);
-           // do nothing */
-        }
-        else if (this.isRed(n.uncle())) {
-            this.repairCase3(n);
-        }
-        else {
-            this.repairCase4(n);
-        }
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
-     * @param {*} n - node
-     */
-    repairCase1(n) {
-        n.color = BLACK;
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
-     * @param {*} n - node
-     */
-    repairCase3(n) {
-        n.parent.color = BLACK;
-        n.uncle().color = BLACK;
-        n.grandparent().color = RED;
-        this.insertRepairTree(n.grandparent());
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Insertion
-     * @param {*} n - node
-     */
-    repairCase4(n) {
-        let p = n.parent;
-        let g = n.grandparent();
-
-        let nr = null;
-        if ((g.left !== null)
-            && (n === g.left.right)) {
-            this.rotateLeft(p);
-            n = n.left;
-        }
-        else if ((g.right !== null)
-            && (n === g.right.left)) {
-            this.rotateRight(p);
-            n = n.right;
-        }
-
-        p = n.parent;
-        g = n.grandparent();
-        if (n === p.left) {
-            this.rotateRight(g);
-        }
-        else {
-            this.rotateLeft(g);
-        }
-
-        p.color = BLACK;
-        g.color = RED;
-    }
-
-    /**
-     * @returns the node with the highest key for the subtree of the specified root node
-     * @param {*} node - root node of the subtree to be evaluated
-     */
-    fetchMaximum(node) {
-        while (!this.isLeaf(node.right)) {
-            node = node.right;
-        }
-
-        return node;
-    }
-
-    /**
-     * @returns the node with the lowest key for the subtree of the specified root node
-     * @param {*} node - root node of the subtree to be evaluated
-     */
-    fetchMinimum(node) {
-        while (!this.isLeaf(node.left)) {
-            node = node.left;
-        }
-
-        return node;
-    }
-
-    /* ===========================
-       ERASE
-       =========================== */
-    /**
-     * Removes node from the tree
-     * @param {*} node
-     */
-    erase(node) {
-        if (this.isLeaf(node)) {
-            return;
-        }
-
-        this.eraseInternal(node);
-        let h = this.head;
-        h.size = h.size - 1;
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
-     * @param {*} node - node
-     */
-    eraseInternal(node) {
-        if (!this.isLeaf(node.left)
-            && !this.isLeaf(node.right)) {
-            let pred = this.fetchMaximum(node.left);
-
-            this.valuePolicy.copy(node, pred);
-            node = pred;
-        }
-
-        let child = (this.isLeaf(node.right)) ? node.left : node.right;
-
-        if (this.isBlack(node)) {
-            this.eraseCase1(node);
-        }
-        this.replaceNode(node, child);
-        if (this.head.size === 2) {
-            if (!this.isLeaf(child)) {
-                // Root node must be BLACK
-                child.color = BLACK;
-            }
-        }
-
-        let h = this.head;
-        if (this.isLeaf(child)) {
-            /* The node didn't have children and it was removed
-               the head needs to update leftmost, rightmost pointers */
-            if (h.leftmost === node) {
-                let p = node.parent;
-                if (p !== null) {
-                    h.leftmost = p;
-                    p.left = h;
-                }
-                else {
-                    h.leftmost = h;
-                }
-            }
-            if (h.rightmost === node) {
-                let p = node.parent;
-                if (p !== null) {
-                    h.rightmost = p;
-                    p.right = h;
-                }
-                else {
-                    h.rightmost = h;
-                }
-            }
-        }
-        else {
-            // the node had a child. Now node is removed. Any references should point to the child now
-            if (h.leftmost === node) {
-                h.leftmost = child;
-                child.left = h;
-            }
-            if (h.rightmost === node) {
-                h.rightmost = child;
-                child.right = h;
-            }
-        }
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
-     * @param {*} node
-     */
-    eraseCase1(node) {
-        if (node.parent === null) {
-            return;
-        }
-        else {
-            this.eraseCase2(node);
-        }
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
-     * @param {*} node
-     */
-    eraseCase2(node) {
-        let s = node.sibling();
-
-        if (this.isRed(s)) {
-            node.parent.color = RED;
-            s.color = BLACK;
-
-            if (node === node.parent.left) {
-                this.rotateLeft(node.parent);
-            }
-            else {
-                this.rotateRight(node.parent);
-            }
-        }
-        this.eraseCase3(node);
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
-     * @param {*} node
-     */
-    eraseCase3(node) {
-        let s = node.sibling();
-        let p = node.parent;
-        if (this.isBlack(p)
-            && this.isBlack(s)
-            && this.isBlack(s.left)
-            && this.isBlack(s.right)) {
-
-            s.color = RED;
-            this.eraseCase1(p);
-        }
-        else {
-            this.eraseCase4(node);
-        }
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
-     * @param {*} node
-     */
-    eraseCase4(node) {
-        let s = node.sibling();
-        let p = node.parent;
-        if (this.isRed(p)
-            && this.isBlack(s)
-            && this.isBlack(s.left)
-            && this.isBlack(s.right)) {
-
-            s.color = RED;
-            p.color = BLACK;
-        }
-        else {
-            this.eraseCase5(node);
-        }
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
-     * @param {*} node
-     */
-    eraseCase5(node) {
-        let s = node.sibling();
-        let p = node.parent;
-        /* The check below is unnecessary
-           due to case 2 (even though case 2 changed the sibling to a sibling's child,
-           the sibling's child can't be red, since no red parent can have a red child). */
-        /* if ((!this.isLeaf(s))
-               && this.isBlack(s)) { */
-
-        /* the following statements just force the red to be on the left of the left of the parent,
-           or right of the right, so case six will rotate correctly. */
-        if (node === p.left
-            && this.isRed(s.left)
-			&& this.isBlack(s.right)) {
-
-            s.color = RED;
-            s.left.color = BLACK;
-            this.rotateRight(s);
-        }
-        else if (node === p.right
-            && this.isBlack(s.left)
-            && this.isRed(s.right)) {
-
-            s.color = RED;
-            s.right.color = BLACK;
-            this.rotateLeft(s);
-        }
-        //}
-        this.eraseCase6(node);
-    }
-
-    /**
-     * @private
-     * The method is decribed at: https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Removal
-     * @param {*} node
-     */
-    eraseCase6(node) {
-        let s = node.sibling();
-        let p = node.parent;
-        s.color = this.fetchColor(p);
-        p.color = BLACK;
-
-        if (node === p.left) {
-            s.right.color = BLACK;
-            this.rotateLeft(p);
-        }
-        else {
-            s.left.color = BLACK;
-            this.rotateRight(p);
-        }
-    }
-
-    /* ===========================
-       SEARCH BY KEY
-       =========================== */
-    /**
-    * @returns an iterator pointin to a node with matching key value. If node is not found then end() iterator is returned.
-    * @param {*} k - key value
-    */
-    find(k) {
-        let y = this.head;
-        let x = y.root;
-        while (!this.isLeaf(x)) {
-            let rc = this.compare(x.key, k);
-            if (rc > 0) {
-                y = x;
-                x = x.left;
-            }
-            else if (rc < 0) {
-                y = x;
-                x = x.right;
-            }
-            else {
-                return new Iterator(x, this);
-            }
-        }
-        return new Iterator(this.head, this);
-    }
-
-    /**
-     * @returns an iterator pointing to the first node in the tree that is not less than
-     * (i.e. greater or equal to) the specified key value, or end() if no such node is found.
-     * @param {*} k - key value
-     */
-    lowerBound(k) {
-        let y = this.head;
-        let x = y.root;
-        while (!this.isLeaf(x)) {
-            let rc = this.compare(x.key, k);
-            if (rc >= 0) {
-                y = x;
-                x = x.left;
-            }
-            else {
-                x = x.right;
-            }
-        }
-        return new Iterator(y, this);
-    }
-
-    /**
-     * @returns an iterator pointing to the first node in the tree that is greater than
-     * the specified key value, or end() if no such node is found.
-     * @param {*} k - key value
-     */
-    upperBound(k) {
-        let y = this.head;
-        let x = y.root;
-        while (!this.isLeaf(x)) {
-            let rc = this.compare(x.key, k);
-            if (rc > 0) {
-                y = x;
-                x = x.left;
-            }
-            else {
-                x = x.right;
-            }
-        }
-        return new Iterator(y, this);
-    }
-
-    /* ===========================
-       ITERATORS
-       =========================== */
-
-    /**
-     * @returns iterator pointing to the node with the lowest key
-     */
-    begin() {
-        return new Iterator(this.head.leftmost, this);
-    }
-
-    /**
-     * @returns iterator pointing to the node following the node with the highest key
-     */
-    end() {
-        return new Iterator(this.head, this);
-    }
-
-    /**
-     * @returns iterator pointing to the node with the highest key
-     */
-    rbegin() {
-        return new ReverseIterator(this.head.rightmost, this);
-    }
-
-    /**
-     * @returns iterator pointing to the node preceding the node with the lowest key
-     */
-    rend() {
-        return new ReverseIterator(this.head, this);
-    }
-
-    /**
-     * @private
-     * provides support for ES6 forward iteration
-     */
-    jsBegin() {
-        return this.head.leftmost;
-    }
-
-    /**
-     * @private
-     * provides support for ES6 forward iteration
-     */
-    jsEnd() {
-        return this.head;
-    }
-
-    /**
-     * @private
-     * provides support for ES6 reverse iteration
-     */
-    jsRbegin() {
-        return this.head.rightmost;
-    }
-
-    /**
-     * @private
-     * provides support for ES6 forward iteration
-     */
-    jsRend() {
-        return this.head;
-    }
-
-    /**
-     * @returns node following the specified node in ascending order of their keys
-     * @param {*} n - node
-     */
-    next(n) {
-        if (n === this.head) {
-            return this.head.leftmost;
-        }
-        if (n.right === this.head) {
-            return this.head;
-        }
-        if (n.right !== null) {
-            let res = this.fetchMinimum(n.right);
-            return res;
-        }
-        else {
-            while (n.parent.left !== n) {
-                n = n.parent;
-            }
-            return n.parent;
-        }
-    }
-
-    /**
-     * @returns node preceding the specified node in ascending order of their keys
-     * @param {*} n - node
-     */
-    prev(n) {
-        if (n === this.head) {
-            return this.head.rightmost;
-        }
-        if (n.left === this.head) {
-            return this.head;
-        }
-        if (n.left !== null) {
-            let res = this.fetchMaximum(n.left);
-            return res;
-        }
-        else {
-            while (n.parent.right !== n) {
-                n = n.parent;
-            }
-            return n.parent;
-        }
-    }
-
-    /**
-     * ES6 forward iteration
-     */
-    [Symbol.iterator]() {
-        return new JsIterator(this);
-    }
-
-    /**
-     * ES6 reverse iteration
-     */
-    backward() {
-        return new JsReverseIterator(this);
-    }
-
-    /**
-     * @returns a new JsIterator object that contains the [key, value] pairs for each element in the order of the keys.
-     */
-    entries() {
-        return new JsIterator(this);
-    }
-
-    /**
-     * @returns a new JsIterator object that contains the keys for each element in the order of the keys.
-     */
-    keys() {
-        return new JsIterator(this, new KeyOnlyPolicy());
-    }
-
-    /**
-     * @returns a new JsIterator object that contains the values for each element in the order of the keys.
-     */
-    values() {
-        return new JsIterator(this, new ValueOnlyPolicy());
-    }
-
-    /**
-     * @returns first element of the container, or undefined if container is empty
-     */
-    first() {
-        if (this.size() === 0) {
-            return undefined;
-        }
-        else {
-            let it = this.begin();
-            return this.valuePolicy.fetch(it.node);
-        }
-    }
-
-    /**
-     * @returns last element of the container, or undefined if container is empty
-     */
-    last() {
-        if (this.size() === 0) {
-            return undefined;
-        }
-        else {
-            let it = this.rbegin();
-            return this.valuePolicy.fetch(it.node);
-        }
-    }
-
-    /**
-     * @returns String representation of the container
-     */
-    toString() {
-        let parts = [];
-        for (let it = this.begin(); !it.equals(this.end()); it.next()) {
-            // convert each key-value pair
-            parts.push(this.valuePolicy.toString(it.node));
-        }
-        return '{' + parts.join(',') + '}';
-    }
-
-    /**
-     * @returns String tag of this class
-     */
-    get [Symbol.toStringTag]() {
-        return 'Tree';
-    }
-
-    /**
-     * @returns constructor object for this class
-     */
-    static get [Symbol.species]() {
-        return Tree;
-    }
-
-}
-
-module.exports = {
-    Tree: Tree,
-    compare: compare
-};
-
-
-/***/ }),
-/* 3 */
-/***/ (function(module, exports, __nested_webpack_require_31214__) {
-
-"use strict";
-
-
-/* Containers are expected to support the following methods:
-   jsBegin() - returns the very first node
-   jsEnd() - returns the node beyond the last one
-   next(node) - returns the next node
-   prev(node) - returns the previous node
-   valuePolicy - an instance of KeyOnlyPolicy, or KeyValuePolicy */
-/**
-  * ES6-style forward iterator.
-  *
-  * @example
-  * let m = new TreeMap();
-  * ...
-  * for (let [key, value] of m) {
-  *   console.log(`key: ${key}, value: ${value}`);
-  * }
-  * // iterate values
-  * for (let value of m.values()) {
-  *   console.log(`value: ${value}`);
-  * }
-  */
-class JsIterator {
-    /**
-     * @param {*} container
-     */
-    constructor(container, valuePolicy = container.valuePolicy) {
-        /**
-         * @private
-         * Internal reference to a container
-         */
-        this.container = container;
-        /**
-         * @private
-         * valuePolicy implements what members of the node will be returned: key, value, or key and value
-         */
-        this.valuePolicy = valuePolicy;
-        /**
-         * @private
-         * current node
-         */
-        this.node = container.jsBegin();
-    }
-
-    /**
-     * As documented in ES6 iteration protocol. It can be used for manual iteration.
-     * Iterators are documented here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
-     *
-     * @example
-     * let m = new TreeMap();
-     * ...
-     * let jsIt = m.entries();
-     * while (true) {
-     *   let res = it.next();
-     *   if (res.done) {
-     *     break;
-     *   }
-     *   console.log(`key: ${res.value[0]}, value: ${res.value[1]`});
-     * }
-     */
-    next() {
-        let res = {};
-        res.done = (this.node === this.container.jsEnd());
-        if (!res.done) {
-            res.value = this.valuePolicy.fetch(this.node);
-            this.node = this.container.next(this.node);
-        }
-        return res;
-    }
-
-    /**
-     * Support for ES6 for-of loops.
-     * @returns {JsIterator}
-     */
-    [Symbol.iterator]() {
-        return this;
-    }
-
-    /**
-     * A reverse iterator for the same container.
-     * @returns {JsReverseIterator}
-     * @example
-     * let m = new TreeMap();
-     * ...
-     * // iterate all key-value pairs in reverse order
-     * for (let [key, value] of m.backwards()) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * }
-    */
-    backwards() {
-        // eslint-disable-next-line no-use-before-define
-        return new JsReverseIterator(this.container, this.valuePolicy);
-    }
-}
-
-/* Containers are expected to support the following methods:
-   jsRbegin() - returns the very first node in reverse order (e.g. the very last node)
-   jsrEnd() - returns the node beyond the last one in reverse order (e.g. the node before the first one)
-   next(node) - returns the next node
-   prev(node) - returns the previous node
-   valuePolicy - an instance of KeyOnlyPolicy, or KeyValuePolicy */
-/**
-  * ES6-style backward iterator
-  * @example
-  * let m = new TreeMap();
-  * ...
-  * // iterate all key-value pairs in reverse order
-  * for (let [key, value] of m.backwards()) {
-  *   console.log(`key: ${key}, value: ${value}`);
-  * }
-  * // iterate keys in reverse order
-  * for (let key of m.keys().backwards()) {
-  *   console.log(`key: ${key}`);
-  * }
- */
-class JsReverseIterator {
-    /**
-     * @param {*} container
-     */
-    constructor(container, valuePolicy = container.valuePolicy) {
-        /**
-         * @private
-         * Internal reference to a container
-         */
-        this.container = container;
-        /**
-         * @private
-         * valuePolicy implements what members of the node will be returned: key, value, or key and value
-         */
-        this.valuePolicy = valuePolicy;
-        /**
-         * @private
-         * current node
-         */
-        this.node = container.jsRbegin();
-    }
-
-    /**
-     * As documented in ES6 iteration protocol. It can be used for manual iteration.
-     * Iterators are documented here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators
-     *
-     * @example
-     * let m = new TreeMap();
-     * ...
-     * let jsIt = m.entries().backwards();
-     * while (true) {
-     *   let res = it.next();
-     *   if (res.done) {
-     *     break;
-     *   }
-     *   console.log(`key: ${res.value[0]}, value: ${res.value[1]`});
-     * }
-     */
-    next() {
-        let res = {};
-        res.done = (this.node === this.container.jsRend());
-        if (!res.done) {
-            res.value = this.valuePolicy.fetch(this.node);
-            this.node = this.container.prev(this.node);
-        }
-        return res;
-    }
-
-    /**
-     * Support for ES6 for-of loops.
-     * @returns {JsReverseIterator}
-     */
-    [Symbol.iterator]() {
-        return this;
-    }
-
-    /**
-     * A forward iterator for the same container
-     * @returns {JsIterator}
-     * @example
-     * let m = new TreeMap();
-     * ...
-     * // iterate all key-value pairs in direct order
-     * for (let [key, value] of m.backwards().backwards()) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     */
-    backwards() {
-        return new JsIterator(this.container, this.valuePolicy);
-    }
-}
-
-module.exports = {
-    JsIterator: JsIterator,
-    JsReverseIterator: JsReverseIterator
-};
-
-/***/ }),
-/* 4 */
-/***/ (function(module, exports, __nested_webpack_require_36806__) {
-
-"use strict";
-
-/**
- * Base class for STL-like iterators. It references a node (or index) and a container.
- * Navigation is achieved by calling container's prev() and next() methods.
- */
-class BaseIterator {
-    /**
-     * @param {*} node - current node
-     * @param {*} container - container
-     */
-    constructor(node, container) {
-        /**
-         * @private
-         * __n - internal node reference
-         */
-        this.__n = node;
-        /**
-         * @private
-         * __c - internal container reference
-         */
-        this.__c = container;
-    }
-
-    /**
-     * Two iterators are considered to be equal if they point to the same node of the same container
-     * @param {BaseIterator} rhs - object on the 'right-hand side' of .eq. operator
-     * @returns {boolean}
-     */
-    equals(rhs) {
-        let lhsClass = this.constructor.name;
-        let rhsClass = rhs.constructor.name;
-        if (lhsClass !== rhsClass) {
-            throw new Error(`Can't compare an instance of ${lhsClass} with an instance of ${rhsClass}`);
-        }
-        if (this.__c !== rhs.__c) {
-            throw new Error('Iterators belong to different containers');
-        }
-        return this.__n === rhs.__n;
-    }
-
-    /**
-     * @private
-     * @returns current node
-     */
-    get node() {
-        return this.__n;
-    }
-
-    /**
-     * @private
-     * @returns key of the current node
-     */
-    get key() {
-        return this.__n.key;
-    }
-
-    /**
-     * @private
-     * @returns value of the current node
-     */
-    get value() {
-        return this.__n.value;
-    }
-
-    /**
-     * @private
-     * @returns container that holds current node
-     */
-    get container() {
-        return this.__c;
-    }
-}
-
-/**
- * STL-like forward iterator. It's more verbose than ES6 iterators, but allows iteration over any part of the container
- *
- * @example
- * let m = new TreeMap();
- * ...
- * for (let it = m.begin(); !it.equals(m.end()); it.next()) {
- *   console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
-class Iterator extends BaseIterator {
-    /**
-     * There are 3 ways to construct an iterator:
-     *
-     * 1. Using a node and a container
-     * 2. Copy constructor / clone
-     * 3. Copy constructor / clone from ReverseIterator instance
-     * @param {*} args
-     *
-     * @example
-     * // Using a node and a container
-     * let it = new Iterator(node, container);
-     *
-     * // Copy constructor / clone
-     * let it1 = new Iterator(node, container);
-     * let it2 = new Iterator(it1);
-     *
-     * // Copy constructor / clone from ReverseIterator instance
-     * let it1 = new ReverseIterator(node, container);
-     * let it2 = new Iterator(it1);
-     */
-    constructor(...args) {
-        if (args.length === 2) {
-            let [node, container] = args;
-            super(node, container);
-        }
-        else if (args.length === 1) {
-            let [obj] = args;
-            let className = obj.constructor.name;
-            if (className === Iterator.name) {
-                super(obj.__n, obj.__c);
-            }
-            // eslint-disable-next-line no-use-before-define
-            else if (className === ReverseIterator.name) {
-                let c = obj.__c;
-                super(c.next(obj.__n), c);
-            }
-            else {
-                throw new Error(`Can't create an Iterator from ${className}`);
-            }
-        }
-        else {
-            throw new Error('Can\'t create an Iterator with provided parameters');
-        }
-    }
-
-    /**
-     * Replaces node reference with the reference of the next node in the container.
-     * Can be used for manual iteration over a range of key-value pairs.
-     * @example
-     * let m = new TreeMap();
-     * ... // add key-value pairs., using numbers as keys
-     * let from = t.lowerBound(0);
-     * let to = t.upperBound(50);
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     */
-    next() {
-        /**
-         * __n and __c are defined in the base class
-         */
-        this.__n = this.__c.next(this.__n);
-    }
-
-    /**
-     * Replaces node reference with the reference of the previous node in the container
-     * Can be used for manual reverse iteration over a range of key-value pairs.
-     * @example
-     * let m = new TreeMap();
-     * ... // add key-value pairs., using numbers as keys
-     * let from = t.lowerBound(0);
-     * let to = t.upperBound(50);
-     * let it = to;
-     * while (!it.equals(from)) {
-     *   it.prev();
-     *   console.log(it.key);
-     * }
-     */
-    prev() {
-        this.__n = this.__c.prev(this.__n);
-    }
-}
-
-/**
- * STL-like backward iterator. Can be used to traverse container or a range in the reverse order.
- * It's more verbose than ES6 iterators, but allows iteration over any part of the container
- *
- * @example
- * let m = new TreeMap();
- * ...
- * for (let it = m.rbegin(); !it.equals(m.rend()); it.next()) {
- *   console.log(`key: ${it.key}, value: ${it.value}`);
- * }
- */
-class ReverseIterator extends BaseIterator {
-    /**
-     * There are 3 ways to construct a reverse iterator:
-     *
-     * 1. Using a node and a container
-     * 2. Copy constructor / clone
-     * 3. Copy constructor / clone from forward Iterator instance
-     * @param {*} args
-     *
-     * @example
-     * // Using a node and a container
-     * let it = new ReverseIterator(node, container);
-     *
-     * // Copy constructor / clone
-     * let it1 = new ReverseIterator(node, container);
-     * let it2 = new ReverseIterator(it1);
-     *
-     * // Copy constructor / clone from forward Iterator instance
-     * let it1 = new Iterator(node, container);
-     * let it2 = new ReverseIterator(it1);
-     */
-    constructor(...args) {
-        if (args.length === 2) {
-            let [node, container] = args;
-            super(node, container);
-        }
-        else if (args.length === 1) {
-            let [obj] = args;
-            let className = obj.constructor.name;
-            if (className === ReverseIterator.name) {
-                super(obj.__n, obj.__c);
-            }
-            else if (className === Iterator.name) {
-                let c = obj.__c;
-                super(c.prev(obj.__n), c);
-            }
-            else {
-                throw new Error(`Can't create an ReverseIterator from ${className}`);
-            }
-        }
-        else {
-            throw new Error('Can\'t create a Reverse Iterator with provided parameters');
-        }
-    }
-
-    /**
-     *  Replaces node reference with the reference of the previous node in the container, because it works in reverse order
-     * Can be used for manual reverse iteration over a range of key-value pairs.
-     * @example
-     * let m = new TreeMap();
-     * ... // add key-value pairs., using numbers as keys
-     * let from = new ReverseIterator(t.upperBound(50));
-     * let to = new ReverseIterator(t.lowerBound(0));
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     */
-    next() {
-        /**
-         * __n and __c are defined in the base class
-         */
-        this.__n = this.__c.prev(this.__n);
-    }
-
-    /**
-     *  Replaces node reference with the reference of the next node in the container, because it works in reverse order
-     * Can be used for manual forward iteration over a range of key-value pairs.
-     * @example
-     * let m = new TreeMap();
-     * ... // add key-value pairs., using numbers as keys
-     * let from = new ReverseIterator(t.upperBound(50));
-     * let to = new ReverseIterator(t.lowerBound(0));
-     * let it = to;
-     * while (!it.equals(from)) {
-     *   it.prev();
-     *   console.log(it.key);
-     * }
-     */
-    prev() {
-        this.__n = this.__c.next(this.__n);
-    }
-}
-
-module.exports = {
-    Iterator: Iterator,
-    ReverseIterator: ReverseIterator
-};
-
-/***/ }),
-/* 5 */
-/***/ (function(module, exports, __nested_webpack_require_45031__) {
-
-module.exports = __nested_webpack_require_45031__(6);
-
-
-/***/ }),
-/* 6 */
-/***/ (function(module, exports, __nested_webpack_require_45149__) {
-
-/* This is an entry point to the library.
-   It collects all public classes and re-exports them */
-/**@private */
-const {TreeMap} = __nested_webpack_require_45149__(7);
-/**@private */
-const {TreeMultiMap} = __nested_webpack_require_45149__(9);
-/**@private */
-const {TreeSet} = __nested_webpack_require_45149__(10);
-/**@private */
-const {TreeMultiSet} = __nested_webpack_require_45149__(11);
-/**@private */
-const {Iterator, ReverseIterator} = __nested_webpack_require_45149__(4);
-/**@private */
-const {JsIterator, JsReverseIterator} = __nested_webpack_require_45149__(3);
-
-module.exports = {
-    Iterator: Iterator,
-    ReverseIterator: ReverseIterator,
-    JsIterator: JsIterator,
-    JsReverseIterator: JsReverseIterator,
-    TreeMap: TreeMap,
-    TreeMultiMap: TreeMultiMap,
-    TreeSet: TreeSet,
-    TreeMultiSet: TreeMultiSet,
-};
-
-/***/ }),
-/* 7 */
-/***/ (function(module, exports, __nested_webpack_require_46005__) {
-
-/** An implementation of red-black tree */
-const {Tree} = __nested_webpack_require_46005__(2);
-/** Classes that regulate whether tree nodes hold keys only, or key-value pairs */
-const {KeyValuePolicy} = __nested_webpack_require_46005__(1);
-/** Node for a red-black tree */
-const {TreeNode} = __nested_webpack_require_46005__(0);
-
-/**
- * TreeMap is an associative container that stores elements formed
- * by a combination of a key value and a mapped value, following a specific order.
- *
- * In a TreeMap, the key values are generally used to sort and uniquely identify
- * the elements, while the mapped values store the content associated to this key.
- * The types of key and mapped value may differ.
- *
- * ## Container properties
- * * **Associative** - Elements in associative containers are referenced by their key
- * and not by their absolute position in the container.
- * * **Ordered** - The elements in the container follow a strict order at all times.
- * All inserted elements are given a position in this order.
- * * **Map** - Each element associates a key to a mapped value. Keys are meant
- * to identify the elements whose main content is the mapped value.
- * * **Unique keys** - No two elements in the container can have equivalent keys.
- *
- * @example
- * let map = new TreeMap();
- * // add few values
- * map.set(1, 'a');
- * map.set(2, 'b');
- * // find a value by key
- * let v = map.get(1); // << 'a'
- * // print all key-value pairs
- * for (let [key, value] of map) {
- *   console.log(`key: ${key}, value: ${value}`);
- * }
- */
-class TreeMap {
-    /*======================================================
-     * Methods of ES6 Map
-     *======================================================*/
-
-    /**
-     * Creates an empty, or a pre-initialized map.
-     * @param {*} [iterable] Another iterable object whose key-value pairs are added into the newly created map.
-     * @example
-     * // Create an empty map
-     * let map1 = new TreeMap();
-     * // Create and initialize map
-     * let map2 = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     */
-    constructor(iterable) {
-        /** Internal tree */
-        this.__t = new Tree();
-        this.__t.valuePolicy = new KeyValuePolicy();
-        if ((iterable !== undefined)
-            && (iterable !== null)) {
-            if (iterable[Symbol.iterator] !== undefined) {
-                // copy contents
-                for (let [k, v] of iterable) {
-                    this.set(k, v);
-                }
-            }
-            else {
-                throw new Error('TreeMap constructor accepts only iterable objects');
-            }
-        }
-    }
-
-    /**
-     * String tag of this class
-     * @returns {String}
-     * @example
-     * Object.prototype.toString.call(new TreeMap()); // "[object TreeMap]"
-     */
-    get [Symbol.toStringTag]() {
-        return 'TreeMap';
-    }
-
-    /**
-     * Allows to create programmatically an instance of the same class
-     * @returns constructor object for this class.
-     * @example
-     * let map = new TreeMap();
-     * let constrFunc = Object.getPrototypeOf(map).constructor[Symbol.species];
-     * let map2 = new constrFunc();
-     */
-    static get [Symbol.species]() {
-        return TreeMap;
-    }
-
-    /**
-     * Removes all key-value pairs.
-     * @example
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * map.clear();
-     * console.log(map.size); // 0
-     */
-    clear() {
-        this.__t.clear();
-    }
-
-    /**
-     * Removes key-value pair with the specified key if such entry exists. Does nothing otherwise.
-     * @example
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * map.delete(2);
-     * console.log(map.toString()); // {1:A,3:C}
-     */
-    delete(key) {
-        let it = this.__t.find(key);
-        if (!it.equals(this.__t.end())) {
-            this.__t.erase(it.node);
-        }
-    }
-
-    /**
-     * Forward ES6 iterator for all key-value pairs in ascending order of the keys.
-     * @returns {JsIterator}
-     * @example
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let [key,value] of map.entries()) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * }
-     */
-    entries() {
-        return this.__t.entries();
-    }
-
-    /**
-     * Iterates all key-value pairs using a callback in ascending order of the keys.
-     * Note that ES6 specifies the order of key value parameters in the callback differently from for-of loop.
-     * @example
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * map.forEach(function(value, key, container) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * });
-     */
-    forEach(callback) {
-        for (let [k, v] of this.__t) {
-            callback(v, k, this);
-        }
-    }
-
-    /**
-     * Finds value associated with the specified key. If specified key does not exist then undefined is returned.
-     * @returns {*}
-     * @param {*} key a value of any type that can be compared with a key
-     * @example
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * let v = map.get(3); // 'C'
-     * * let v = map.get(4); // returns undefined
-     */
-    get(key) {
-        let it = this.__t.find(key);
-        if (!it.equals(this.__t.end())) {
-            return it.value;
-        }
-        else {
-            return undefined;
-        }
-    }
-
-    /**
-     * A boolean indicator whether map contains a key-value pair with the specified key
-     * @returns {Boolean}
-     * @param {*} key a value of any type that can be compared with a key
-     * @example
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * let b = map.get(3); // true
-     */
-    has(key) {
-        let it = this.__t.find(key);
-        if (!it.equals(this.__t.end())) {
-            return true;
-        }
-        else {
-            return false;
-        }
-    }
-
-    /**
-     * Forward ES6 iterator for all keys in ascending order of the keys.
-     * @returns {JsIterator}
-     * @example
-     * // iterate all keys
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let k of map.keys()) {
-     *   console.log(k); // 1, 2, 3
-     * }
-     * // iterate all keys in reverse order
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let k of map.keys().backward()) {
-     *   console.log(k); // 3, 2, 1
-     * }
-     */
-    keys() {
-        return this.__t.keys();
-    }
-
-    /**
-     * Adds or updates key-value pair to the map.
-     * @param {*} key
-     * @param {*} value
-     * @example
-     * let map = new TreeMap();
-     * map.set(1, 'A');
-     */
-    set(key, value) {
-        let n = new TreeNode();
-        n.key = key;
-        n.value = value;
-        this.__t.insertOrReplace(n);
-    }
-
-    /**
-     * Number of key-value pairs in the map.
-     * @returns {Number}
-     */
-    get size() {
-        return this.__t.size();
-    }
-
-    /**
-     * Forward ES6 iterator for all values in ascending order of the keys.
-     * @returns {JsITerator}
-     * @example
-     * // iterate all values
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let v of map.values()) {
-     *   console.log(v); // 'A', 'B', 'C'
-     * }
-     * // iterate all values in reverse order
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let v of map.values().backward()) {
-     *   console.log(v); // 'C', 'B', 'A'
-     * }
-     */
-    values() {
-        return this.__t.values();
-    }
-
-    /**
-     * Forward ES6 iterator for all key-value pairs in ascending order of the keys. The same as entries() method
-     * @returns {JsIterator}
-     * @example
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let [key,value] of map) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * }
-     */
-    [Symbol.iterator]() {
-        return this.__t[Symbol.iterator]();
-    }
-
-    /*======================================================
-     * More methods
-     *======================================================*/
-    /**
-     * ES6 reverse iterator for all key-value pairs in descending order of the keys.
-     * @returns {JsReverseIterator}
-     * @example
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let [key,value] of map.backwards()) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * }
-     */
-    backward() {
-        return this.__t.backward();
-    }
-
-    /**
-     * Sets custom comparison function if key values are not of primitive types.
-     * Callback is a 3-way comparison function accepts two key values (lhs, rhs). It is expected to return
-     *      +1 if the value of rhs is greater than lhs
-     *      -1 if the value of rhs is less than lhs
-     *       0 if values are the same
-     */
-    set compareFunc(func) {
-        this.clear();
-        this.__t.compare = func;
-    }
-
-    /*======================================================
-     * STL-like methods
-     *======================================================*/
-
-    /**
-     * Forward iterator to the first element
-     * @returns {Iterator}
-     * @example
-     * let m = new TreeMap();
-     * ...
-     * for (let it = m.begin(); !it.equals(m.end()); it.next()) {
-     *   console.log(`key: ${it.key}, value: ${it.value}`);
-     * }
-     */
-    begin() {
-        return this.__t.begin();
-    }
-
-    /**
-     * Forward iterator to the element following the last element
-     * @returns {Iterator}
-     * @example
-     * let m = new TreeMap();
-     * ...
-     * for (let it = m.begin(); !it.equals(m.end()); it.next()) {
-     *   console.log(`key: ${it.key}, value: ${it.value}`);
-     * }
-     */
-    end() {
-        return this.__t.end();
-    }
-
-    /**
-     * Finds an element with key equivalent to the specified one. If such key does not exist end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let m = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * ...
-     * let it = m.find(1);
-     * if (!it.equals(m.end())) {
-     *   console.log(`key: ${it.key}, value: ${it.value}`); // 1, 'A'
-     * }
-     */
-    find(key) {
-        return this.__t.find(key);
-    }
-
-    /**
-     * Adds key-value pair if such key does not exist in the map
-     * @param {*} key
-     * @param {*} value
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     * @example
-     * let m = new TreeMap();
-     * let res = m.insertUnique(1, 'A');
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.value}`); // prints A
-     * }
-     * res = m.insertUnique(1, 'B') // this step has no effect on the map
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.key}`); // not executed
-     * }
-     */
-    insertUnique(key, value) {
-        let n = new TreeNode();
-        n.key = key;
-        n.value = value;
-        return this.__t.insertUnique(n);
-    }
-
-    /**
-     * Adds key-value pair if such key does not exist in the map. Replaces value if such key exists
-     * @param {*} key
-     * @param {*} value
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     * @example
-     * let m = new TreeMap();
-     * let res = m.insertOrReplace(1, 'A');
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.value}`); // prints A
-     * }
-     * res = m.insertOrReplace(1, 'B') // replaces value on the existing node
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.key}`); // prints B
-     * }
-     */
-    insertOrReplace(key, value) {
-        let n = new TreeNode();
-        n.key = key;
-        n.value = value;
-        return this.__t.insertOrReplace(n);
-    }
-
-    /**
-     * Removes key-value pair for the specified iterator.
-     * @param {Iterator} iterator
-     * @example
-     * let map = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * let it = map.find(2);
-     * it.prev();
-     * map.erase(it); // removes a node with key 1
-     * console.log(map.toString()); // {2:B,3:C}
-     */
-    erase(iterator) {
-        this.__t.erase(iterator.node);
-    }
-
-    /**
-     * Iterator pointing to the first element that is not less than specified key. If no such element is found, see end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let m = new TreeMap();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive
-     * let from = m.lowerBound(0);
-     * let to = m.upperBound(50);
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     *
-     * let m = new TreeMap();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
-     * let from = new ReverseIterator(m.upperBound(50));
-     * let to = new ReverseIterator(m.lowerBound(0));
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     */
-    lowerBound(key) {
-        return this.__t.lowerBound(key);
-    }
-
-    /**
-     * Reverse iterator to the last element.
-     * @returns {ReverseIterator}
-     * @example
-     * let m = new TreeMap();
-     * ...
-     * for (let it = m.rbegin(); !it.equals(m.rend()); it.next()) {
-     *   console.log(`key: ${it.key}, value: ${it.value}`);
-     * }
-     */
-    rbegin() {
-        return this.__t.rbegin();
-    }
-
-    /**
-     * Reverse iterator pointing to before the first element.
-     * @returns {ReverseIterator}
-     * @example
-     * let m = new TreeMap();
-     * ...
-     * for (let it = m.rbegin(); !it.equals(m.rend()); it.next()) {
-     *   console.log(`key: ${it.key}, value: ${it.value}`);
-     * }
-     */
-    rend() {
-        return this.__t.rend();
-    }
-
-    /**
-     * Iterator pointing to the first element that is greater than key. If no such element is found end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let m = new TreeMap();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive
-     * let from = m.lowerBound(0);
-     * let to = m.upperBound(50);
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     *
-     * let m = new TreeMap();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
-     * let from = new ReverseIterator(m.upperBound(50));
-     * let to = new ReverseIterator(m.lowerBound(0));
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     */
-    upperBound(key) {
-        return this.__t.upperBound(key);
-    }
-
-    /**
-     * @returns first key/value pair of the container, or undefined if container is empty
-     * @example
-     * let m = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * let first = m.first();
-     * if (first) {
-     *   let key = first[0];   // 1
-     *   let value = first[1]; // 'A'
-     * }
-     */
-    first() {
-        return this.__t.first();
-    }
-
-    /**
-     * @returns last key/value pair of the container, or undefined if container is empty
-     * @example
-     * let m = new TreeMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * let last = m.last();
-     * if (last) {
-     *   let key = last[0];   // 3
-     *   let value = last[1]; // 'C'
-     * }
-     */
-    last() {
-        return this.__t.last();
-    }
-
-    /**
-     * Serializes contents of the map in the form {key1:value1,key2:value2,...}
-     * @returns {String}
-     */
-    toString() {
-        return this.__t.toString();
-    }
-}
-
-module.exports = {
-    TreeMap: TreeMap,
-};
-
-/***/ }),
-/* 8 */
-/***/ (function(module, exports) {
-
-/**
- * An instance of this class reports whether insert operation was successful.
- * if a node was added, or an existing one replaced then an iterator is provided. Otherwise the value of iterator is undefined
- */
-class InsertionResult {
-    /**
-     * Default constructor
-     * @param {Boolean} wasAdded
-     * @param {Boolean} wasReplaced
-     * @param {Iterator} iterator only provided if the node was added, or replaced
-     */
-    constructor(wasAdded, wasReplaced, iterator) {
-        /**
-         * Boolean flag indicating whether an element was added
-         */
-        this.wasAdded = wasAdded;
-        /**
-         * Boolean flag indicating whether an existing node was updated
-         */
-        this.wasReplaced = wasReplaced;
-        /**
-         * {Iterator} instance pointing to the newly added node
-         */
-        this.iterator = iterator;
-    }
-}
-
-module.exports = {
-    InsertionResult: InsertionResult
-};
-
-/***/ }),
-/* 9 */
-/***/ (function(module, exports, __nested_webpack_require_63461__) {
-
-/** An implementation of red-black tree */
-const {Tree} = __nested_webpack_require_63461__(2);
-/** Classes that regulate whether tree nodes hold keys only, or key-value pairs */
-const {KeyValuePolicy} = __nested_webpack_require_63461__(1);
-/** Node for a red-black tree */
-const {TreeNode} = __nested_webpack_require_63461__(0);
-
-/**
- * TreeMultiMap is an associative container that stores elements formed by
- * a combination of a key value and a mapped value, following a specific order,
- * and where multiple elements can have equivalent keys.
- *
- * In a TreeMultiMap, the key values are generally used to sort and uniquely
- * identify the elements, while the mapped values store the content
- * associated to this key. The types of key and mapped value may differ.
- *
- * ## Container properties
- * * **Associative** - Elements in associative containers are referenced
- * by their key and not by their absolute position in the container.
- * * **Ordered** - The elements in the container follow a strict order
- * at all times. All inserted elements are given a position in this order.
- * * **Map** - Each element associates a key to a mapped value. Keys are meant
- * to identify the elements whose main content is the mapped value.
- * * **Multiple equivalent keys** - Multiple elements in the container
- * can have equivalent keys.
- *
- * @example
- * let map = new TreeMultiMap();
- * // add few values
- * map.set(1, 'a');
- * map.set(2, 'b');
- * map.set(2, 'c');
- * // find a value by key
- * let v = map.get(1); // << 'a'
- * find all values for a given key
- * // print all key-value pairs
- * let from = map.lowerBound(2);
- * let to = map.upperBound(2);
- * let it = from;
- * while (!it.equals(to)) {
- *   console.log(it.key);
- *   it.next();
- * }
- */
-class TreeMultiMap {
-    /*======================================================
-     * Methods of ES6 Map
-     *======================================================*/
-
-    /**
-     * Creates an empty, or a pre-initialized map.
-     * @param {*} [iterable] Another iterable object whose key-value pairs are added into the newly created map.
-     * @example
-     * // Create an empty map
-     * let map1 = new TreeMultiMap();
-     * // Create and initialize map
-     * let map2 = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     */
-    constructor(iterable) {
-        /** Internal tree */
-        this.__t = new Tree();
-        this.__t.valuePolicy = new KeyValuePolicy();
-        if ((iterable !== undefined)
-            && (iterable !== null)) {
-            if (iterable[Symbol.iterator] !== undefined) {
-                // copy contents
-                for (let [k, v] of iterable) {
-                    this.set(k, v);
-                }
-            }
-            else {
-                throw new Error('TreeMultiMap constructor accepts only iterable objects');
-            }
-        }
-    }
-
-    /**
-     * String tag of this class
-     * @returns {String}
-     * @example
-     * Object.prototype.toString.call(new TreeMultiMap()); // "[object TreeMultiMap]"
-     */
-    get [Symbol.toStringTag]() {
-        return 'TreeMultiMap';
-    }
-
-    /**
-     * Allows to create programmatically an instance of the same class
-     * @returns constructor object for this class.
-     * @example
-     * let map = new TreeMultiMap();
-     * let constrFunc = Object.getPrototypeOf(map).constructor[Symbol.species];
-     * let map2 = new constrFunc();
-     */
-    static get [Symbol.species]() {
-        return TreeMultiMap;
-    }
-
-    /**
-     * Removes all key-value pairs.
-     * @example
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * map.clear();
-     * console.log(map.size); // 0
-     */
-    clear() {
-        this.__t.clear();
-    }
-
-    /**
-     * Removes key-value pair with the specified key if such entry exists. Does nothing otherwise.
-     * @example
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * map.delete(2);
-     * console.log(map.toString()); // {1:A,3:C}
-     */
-    delete(key) {
-        let it = this.__t.find(key);
-        if (!it.equals(this.__t.end())) {
-            this.__t.erase(it.node);
-        }
-    }
-
-    /**
-     * Forward ES6 iterator for all key-value pairs in ascending order of the keys.
-     * @returns {JsIterator}
-     * @example
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let [key,value] of map.entries()) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * }
-     */
-    entries() {
-        return this.__t.entries();
-    }
-
-    /**
-     * Iterates all key-value pairs using a callback in ascending order of the keys.
-     * Note that ES6 specifies the order of key value parameters in the callback differently from for-of loop.
-     * @example
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * map.forEach(function(value, key, container) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * });
-     */
-    forEach(callback) {
-        for (let [k, v] of this.__t) {
-            callback(v, k, this);
-        }
-    }
-
-    /**
-     * Finds value associated with the specified key. If specified key does not exist then undefined is returned.
-     * @returns {*}
-     * @param {*} key a value of any type that can be compared with a key
-     * @example
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * let v = map.get(3); // 'C'
-     * * let v = map.get(4); // returns undefined
-     */
-    get(key) {
-        let it = this.__t.find(key);
-        if (!it.equals(this.__t.end())) {
-            return it.value;
-        }
-        else {
-            return undefined;
-        }
-    }
-
-    /**
-     * A boolean indicator whether map contains a key-value pair with the specified key
-     * @returns {Boolean}
-     * @param {*} key a value of any type that can be compared with a key
-     * @example
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * let b = map.get(3); // true
-     */
-    has(key) {
-        let it = this.__t.find(key);
-        if (!it.equals(this.__t.end())) {
-            return true;
-        }
-        else {
-            return false;
-        }
-    }
-
-    /**
-     * Forward ES6 iterator for all keys in ascending order of the keys.
-     * @returns {JsIterator}
-     * @example
-     * // iterate all keys
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let k of map.keys()) {
-     *   console.log(k); // 1, 2, 3
-     * }
-     * // iterate all keys in reverse order
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let k of map.keys().backward()) {
-     *   console.log(k); // 3, 2, 1
-     * }
-     */
-    keys() {
-        return this.__t.keys();
-    }
-
-    /**
-     * Adds a key-value pair to the map. Multiple key-value pairs with the same key are allowed in TreeMultiMap.
-     * @param {*} key
-     * @param {*} value
-     * @example
-     * let map = new TreeMultiMap();
-     * map.set(1, 'A');
-     * map.set(1, 'B');
-     * map.set(2, 'C');
-     * for (let k of map.values()) {
-     *   console.log(k); // A, B, C
-     * }
-     */
-    set(key, value) {
-        let n = new TreeNode();
-        n.key = key;
-        n.value = value;
-        this.__t.insertMulti(n);
-    }
-
-    /**
-     * Number of key-value pairs in the map.
-     * @returns {Number}
-     */
-    get size() {
-        return this.__t.size();
-    }
-
-    /**
-     * Forward ES6 iterator for all values in ascending order of the keys.
-     * @returns {JsITerator}
-     * @example
-     * // iterate all values
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let v of map.values()) {
-     *   console.log(v); // 'A', 'B', 'C'
-     * }
-     * // iterate all values in reverse order
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let v of map.values().backward()) {
-     *   console.log(v); // 'C', 'B', 'A'
-     * }
-     */
-    values() {
-        return this.__t.values();
-    }
-
-    /**
-     * Forward ES6 iterator for all key-value pairs in ascending order of the keys. The same as entries() method
-     * @returns {JsIterator}
-     * @example
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let [key,value] of map) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * }
-     */
-    [Symbol.iterator]() {
-        return this.__t[Symbol.iterator]();
-    }
-
-    /*======================================================
-     * More methods
-     *======================================================*/
-    /**
-     * ES6 reverse iterator for all key-value pairs in descending order of the keys.
-     * @returns {JsReverseIterator}
-     * @example
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * for (let [key,value] of map.backwards()) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * }
-     */
-    backward() {
-        return this.__t.backward();
-    }
-
-    /**
-     * Sets custom comparison function if key values are not of primitive types.
-     * Callback is a 3-way comparison function accepts two key values (lhs, rhs). It is expected to return
-     *      +1 if the value of rhs is greater than lhs
-     *      -1 if the value of rhs is less than lhs
-     *       0 if values are the same
-     */
-    set compareFunc(func) {
-        this.clear();
-        this.__t.compare = func;
-    }
-
-    /*======================================================
-     * STL-like methods
-     *======================================================*/
-
-    /**
-     * Forward iterator to the first element
-     * @returns {Iterator}
-     * @example
-     * let m = new TreeMultiMap();
-     * ...
-     * for (let it = m.begin(); !it.equals(m.end()); it.next()) {
-     *   console.log(`key: ${it.key}, value: ${it.value}`);
-     * }
-     */
-    begin() {
-        return this.__t.begin();
-    }
-
-    /**
-     * Forward iterator to the element following the last element
-     * @returns {Iterator}
-     * @example
-     * let m = new TreeMultiMap();
-     * ...
-     * for (let it = m.begin(); !it.equals(m.end()); it.next()) {
-     *   console.log(`key: ${it.key}, value: ${it.value}`);
-     * }
-     */
-    end() {
-        return this.__t.end();
-    }
-
-    /**
-     * Finds an element with key equivalent to the specified one. If such key does not exist end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let m = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * ...
-     * let it = m.find(1);
-     * if (!it.equals(m.end())) {
-     *   console.log(`key: ${it.key}, value: ${it.value}`); // 1, 'A'
-     * }
-     */
-    find(key) {
-        return this.__t.find(key);
-    }
-
-    /**
-     * Adds key-value pair if such key does not exist in the map
-     * @param {*} key
-     * @param {*} value
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     * @example
-     * let m = new TreeMultiMap();
-     * let res = m.insertUnique(1, 'A');
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.value}`); // prints A
-     * }
-     * res = m.insertUnique(1, 'B') // this step has no effect on the map
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.key}`); // not executed
-     * }
-     */
-    insertUnique(key, value) {
-        let n = new TreeNode();
-        n.key = key;
-        n.value = value;
-        return this.__t.insertUnique(n);
-    }
-
-    /**
-     * Adds key-value pair if such key does not exist in the map. Replaces value if such key exists
-     * @param {*} key
-     * @param {*} value
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     * @example
-     * let m = new TreeMultiMap();
-     * let res = m.insertOrReplace(1, 'A');
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.value}`); // prints A
-     * }
-     * res = m.insertOrReplace(1, 'B') // replaces value on the existing node
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.key}`); // prints B
-     * }
-     */
-    insertOrReplace(key, value) {
-        let n = new TreeNode();
-        n.key = key;
-        n.value = value;
-        return this.__t.insertOrReplace(n);
-    }
-
-    /**
-     * Adds key-value pair. If such key already exists in the map then adds another node with the same key and a new value.
-     * @param {*} key
-     * @param {*} value
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     * @example
-     * let m = new TreeMultiMap();
-     * let res = m.insertMulti(1, 'A');
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.value}`); // prints A
-     * }
-     * res = m.insertMulti(1, 'B') // adds a new node
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.value}`); // prints B
-     *   it.prev();
-     *   console.log(`Previously inserted ${res.iterator.value}`); // prints A
-     * }
-     */
-    insertMulti(key, value) {
-        let n = new TreeNode();
-        n.key = key;
-        n.value = value;
-        return this.__t.insertMulti(n);
-    }
-
-    /**
-     * Removes key-value pair for the specified iterator.
-     * @param {Iterator} iterator
-     * @example
-     * let map = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * let it = map.find(2);
-     * it.prev();
-     * map.erase(it); // removes a node with key 1
-     * console.log(map.toString()); // {2:B,3:C}
-     */
-    erase(iterator) {
-        this.__t.erase(iterator.node);
-    }
-
-    /**
-     * Iterator pointing to the first element that is not less than specified key. If no such element is found, see end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let m = new TreeMultiMap();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive
-     * let from = m.lowerBound(0);
-     * let to = m.upperBound(50);
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     *
-     * let m = new TreeMultiMap();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
-     * let from = new ReverseIterator(m.upperBound(50));
-     * let to = new ReverseIterator(m.lowerBound(0));
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     */
-    lowerBound(key) {
-        return this.__t.lowerBound(key);
-    }
-
-    /**
-     * Reverse iterator to the last element.
-     * @returns {ReverseIterator}
-     * @example
-     * let m = new TreeMultiMap();
-     * ...
-     * for (let it = m.rbegin(); !it.equals(m.rend()); it.next()) {
-     *   console.log(`key: ${it.key}, value: ${it.value}`);
-     * }
-     */
-    rbegin() {
-        return this.__t.rbegin();
-    }
-
-    /**
-     * Reverse iterator pointing to before the first element.
-     * @returns {ReverseIterator}
-     * @example
-     * let m = new TreeMultiMap();
-     * ...
-     * for (let it = m.rbegin(); !it.equals(m.rend()); it.next()) {
-     *   console.log(`key: ${it.key}, value: ${it.value}`);
-     * }
-     */
-    rend() {
-        return this.__t.rend();
-    }
-
-    /**
-     * Iterator pointing to the first element that is greater than key. If no such element is found end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let m = new TreeMultiMap();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive
-     * let from = m.lowerBound(0);
-     * let to = m.upperBound(50);
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     *
-     * let m = new TreeMultiMap();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
-     * let from = new ReverseIterator(m.upperBound(50));
-     * let to = new ReverseIterator(m.lowerBound(0));
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     */
-    upperBound(key) {
-        return this.__t.upperBound(key);
-    }
-
-    /**
-     * @returns first key/value pair of the container, or undefined if container is empty
-     * @example
-     * let m = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * let first = m.first();
-     * if (first) {
-     *   let key = first[0];   // 1
-     *   let value = first[1]; // 'A'
-     * }
-     */
-    first() {
-        return this.__t.first();
-    }
-
-    /**
-     * @returns last key/value pair of the container, or undefined if container is empty
-     * @example
-     * let m = new TreeMultiMap([[1, 'A'], [2, 'B'], [3, 'C']]);
-     * let last = m.last();
-     * if (last) {
-     *   let key = last[0];   // 3
-     *   let value = last[1]; // 'C'
-     * }
-     */
-    last() {
-        return this.__t.last();
-    }
-
-    /**
-     * Serializes contents of the map in the form {key1:value1,key2:value2,...}
-     * @returns {String}
-     */
-    toString() {
-        return this.__t.toString();
-    }
-}
-
-module.exports = {
-    TreeMultiMap: TreeMultiMap,
-};
-
-/***/ }),
-/* 10 */
-/***/ (function(module, exports, __nested_webpack_require_81431__) {
-
-/** An implementation of red-black tree */
-const {Tree} = __nested_webpack_require_81431__(2);
-/** Classes that regulate whether tree nodes hold keys only, or key-value pairs */
-const {KeyOnlyPolicy} = __nested_webpack_require_81431__(1);
-/** Node for a red-black tree */
-const {TreeNode} = __nested_webpack_require_81431__(0);
-
-/**
- * TreeSet is a container that stores unique elements following a specific order.
- *
- * In a TreeSet, the value of an element also identifies it (the value is itself the key),
- * and each value must be unique. The value of the elements in a TreeSet cannot be modified
- * once in the container (the elements are immutable), but they can be inserted or removed
- * from the container.
- *
- * ## Container properties
- * * **Associative** - Elements in associative containers are referenced by their key and
- * not by their absolute position in the container.</li>
- * * **Ordered** - The elements in the container follow a strict order at all times.
- * All inserted elements are given a position in this order.</li>
- * * **Set** - The value of an element is also the key used to identify it.</li>
- * * **Unique keys** - No two elements in the container can have equivalent keys.</li>
- *
- *
- * @example
- * let set = new TreeSet();
- * // add few values
- * set.add(1);
- * set.add(2);
- * // check whether key exists
- * let flag = set.has(1); // << true
- * // print all keys
- * for (let key of set) {
- *   console.log(`key: ${key}`);
- * }
- */
-class TreeSet {
-    /*======================================================
-     * Methods of ES6 Set
-     *======================================================*/
-
-    /**
-     * Creates an empty, or a pre-initialized set.
-     * @param {*} [iterable] Another iterable object whose values are added into the newly created set.
-     * @example
-     * // Create an empty set
-     * let set = new TreeSet();
-     * // Create and initialize set
-     * let set2 = new TreeSet([1, 2, 3]);
-     */
-    constructor(iterable) {
-        /** Internal tree */
-        this.__t = new Tree();
-        this.__t.valuePolicy = new KeyOnlyPolicy();
-        if ((iterable !== undefined)
-            && (iterable !== null)) {
-            if (iterable[Symbol.iterator] !== undefined) {
-                // copy contents
-                for (let k of iterable) {
-                    this.add(k);
-                }
-            }
-            else {
-                throw new Error('TreeSet constructor accepts only iterable objects');
-            }
-        }
-    }
-
-    /**
-     * String tag of this class
-     * @returns {String}
-     * @example
-     * Object.prototype.toString.call(new TreeSet()); // "[object TreeSet]"
-     */
-    get [Symbol.toStringTag]() {
-        return 'TreeSet';
-    }
-
-    /**
-     * Allows to create programmatically an instance of the same class
-     * @returns constructor object for this class.
-     * @example
-     * let set = new TreeSet();
-     * let constrFunc = Object.getPrototypeOf(set).constructor[Symbol.species];
-     * let set2 = new constrFunc();
-     */
-    static get [Symbol.species]() {
-        return TreeSet;
-    }
-
-    /**
-     * Removes all key-value pairs.
-     * @example
-     * let set = new TreeSet([1, 2, 3]);
-     * set.clear();
-     * console.log(set.size); // 0
-     */
-    clear() {
-        this.__t.clear();
-    }
-
-    /**
-     * Removes key-value pair with the specified key if such entry exists. Does nothing otherwise.
-     * @example
-     * let set = new TreeSet([1, 2, 3]);
-     * set.delete(2);
-     * console.log(set.toString()); // {1,3}
-     */
-    delete(key) {
-        let it = this.__t.find(key);
-        if (!it.equals(this.__t.end())) {
-            this.__t.erase(it.node);
-        }
-    }
-
-    /**
-     * Forward ES6 iterator for all values in ascending order.
-     * @returns {JsIterator}
-     * @example
-     * let set = new TreeSet([1, 2, 3]);
-     * for (let key of set.entries()) {
-     *   console.log(`key: ${key}`);
-     * }
-     */
-    entries() {
-        return this.__t.entries();
-    }
-
-    /**
-     * Iterates all values using a callback in ascending order.
-     * Note that ES6 specifies the order of key parameters in the callback differently from for-of loop.
-     * @example
-     * let set = new TreeSet([1, 2, 3]);
-     * set.forEach(function(value, key, container) {
-     *   // value is the same as key
-     *   console.log(`key: ${key}, value: ${value}`);
-     * });
-     */
-    forEach(callback) {
-        for (let k of this.__t) {
-            callback(k, k, this);
-        }
-    }
-
-    /**
-     * A boolean indicator whether set contains the specified key.
-     * @returns {Boolean}
-     * @param {*} key a value of any type that can be compared with a key
-     * @example
-     * let set = new TreeSet([1, 2, 3]);
-     * let b = set.get(3); // true
-     * b = set.get(4); // false
-     */
-    has(key) {
-        let it = this.__t.find(key);
-        if (!it.equals(this.__t.end())) {
-            return true;
-        }
-        else {
-            return false;
-        }
-    }
-
-    /**
-     * Forward ES6 iterator for all keys in ascending order.
-     * @returns {JsIterator}
-     * @example
-     * // iterate all keys
-     * let set = new TreeSet([1, 2, 3]);
-     * for (let k of set.keys()) {
-     *   console.log(k); // 1, 2, 3
-     * }
-     * // iterate all keys in reverse order
-     * let set = new TreeSet([1, 2, 3]);
-     * for (let k of set.keys().backward()) {
-     *   console.log(k); // 3, 2, 1
-     * }
-     */
-    keys() {
-        return this.__t.keys();
-    }
-
-    /**
-     * Adds a key to the set, unless the key already exists.
-     * @param {*} key
-     * @example
-     * let set = new TreeSet();
-     * set.add(1);
-     */
-    add(key) {
-        let n = new TreeNode();
-        n.key = key;
-        this.__t.insertUnique(n);
-    }
-
-    /**
-     * Number of keys in the set.
-     * @returns {Number}
-     */
-    get size() {
-        return this.__t.size();
-    }
-
-    /**
-     * Forward ES6 iterator for all keys in ascending order. It is the same as keys() method
-     * @returns {JsITerator}
-     * @example
-     * // iterate all values
-     * let set = new TreeSet([1, 2, 3]);
-     * for (let v of set.values()) {
-     *   console.log(v); // '1', '2', '3'
-     * }
-     * // iterate all values in reverse order
-     * let set = new TreeSet([1, 2, 3]);
-     * for (let v of set.values().backward()) {
-     *   console.log(v); // '3', '2', '1'
-     * }
-     */
-    values() {
-        return this.__t.keys();
-    }
-
-    /**
-     * Forward ES6 iterator for all keys in ascending order. The same as entries() method
-     * @returns {JsIterator}
-     * @example
-     * let set = new TreeSet([1, 2, 3]);
-     * for (let key of set) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * }
-     */
-    [Symbol.iterator]() {
-        return this.__t[Symbol.iterator]();
-    }
-
-    /*======================================================
-     * More methods
-     *======================================================*/
-    /**
-     * ES6 reverse iterator for all keys in descending order.
-     * @returns {JsReverseIterator}
-     * @example
-     * let set = new TreeSet([1, 2, 3]);
-     * for (let key of set.backwards()) {
-     *   console.log(`key: ${key}`);
-     * }
-     */
-    backward() {
-        return this.__t.backward();
-    }
-
-    /**
-     * Sets custom comparison function if key values are not of primitive types.
-     * Callback is a 3-way comparison function accepts two key values (lhs, rhs). It is expected to return
-     *      +1 if the value of rhs is greater than lhs
-     *      -1 if the value of rhs is less than lhs
-     *       0 if values are the same
-     */
-    set compareFunc(func) {
-        this.clear();
-        this.__t.compare = func;
-    }
-
-    /*======================================================
-     * STL-like methods
-     *======================================================*/
-
-    /**
-     * Forward iterator to the first element
-     * @returns {Iterator}
-     * @example
-     * let set = new TreeSet();
-     * ...
-     * for (let it = set.begin(); !it.equals(set.end()); it.next()) {
-     *   console.log(`key: ${it.key}`);
-     * }
-     */
-    begin() {
-        return this.__t.begin();
-    }
-
-    /**
-     * Forward iterator to the element following the last element
-     * @returns {Iterator}
-     * @example
-     * let set = new TreeSet();
-     * ...
-     * for (let it = set.begin(); !it.equals(set.end()); it.next()) {
-     *   console.log(`key: ${it.key}`);
-     * }
-     */
-    end() {
-        return this.__t.end();
-    }
-
-    /**
-     * Finds an element with key equivalent to the specified one. If such key does not exist end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let set = new TreeSet([1, 2, 3]);
-     * ...
-     * let it = set.find(1);
-     * if (!it.equals(set.end())) {
-     *   console.log(`Found key: ${it.key}`); // 1
-     * }
-     */
-    find(key) {
-        return this.__t.find(key);
-    }
-
-    /**
-     * Adds a key if it doesn't exist
-     * @param {*} key
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     * @example
-     * let set = new TreeSet();
-     * let res = set.insertUnique(1);
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.key}`); // prints 1
-     * }
-     * res = set.insertUnique(1); // this step has no effect on the set
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.key}`); // not executed
-     * }
-     */
-    insertUnique(key) {
-        let n = new TreeNode();
-        n.key = key;
-        return this.__t.insertUnique(n);
-    }
-
-    /**
-     * Adds key-value pair if such key does not exist in the map. Replaces value if such key exists
-     * @param {*} key
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     * @example
-     * let set = new TreeSet();
-     * let res = set.insertOrReplace(1);
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.key}`); // prints 1
-     * }
-     * res = set.insertOrReplace(1) // returns iterator to the previously added node
-     * if (res.wasInserted) {
-     *   console.log(`Inserted ${res.iterator.key}`); // prints 1
-     * }
-     */
-    insertOrReplace(key) {
-        let n = new TreeNode();
-        n.key = key;
-        return this.__t.insertOrReplace(n);
-    }
-
-    /**
-     * Removes value for the specified iterator.
-     * @param {Iterator} iterator
-     * @example
-     * let set = new TreeSet([1,2,3]);
-     * let it = set.find(2);
-     * it.prev();
-     * set.erase(it); // removes a node with key 1
-     * console.log(set.toString()); // {2,3}
-     */
-    erase(iterator) {
-        this.__t.erase(iterator.node);
-    }
-
-    /**
-     * Iterator pointing to the first element that is not less than specified key. If no such element is found, see end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let set = new TreeSet();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive
-     * let from = set.lowerBound(0);
-     * let to = set.upperBound(50);
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     *
-     * let set = new TreeSet();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
-     * let from = new ReverseIterator(set.upperBound(50));
-     * let to = new ReverseIterator(set.lowerBound(0));
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     */
-    lowerBound(key) {
-        return this.__t.lowerBound(key);
-    }
-
-    /**
-     * Reverse iterator to the last element.
-     * @returns {ReverseIterator}
-     * @example
-     * let set = new TreeSet();
-     * ...
-     * for (let it = set.rbegin(); !it.equals(set.rend()); it.next()) {
-     *   console.log(`key: ${it.key}`);
-     * }
-     */
-    rbegin() {
-        return this.__t.rbegin();
-    }
-
-    /**
-     * Reverse iterator pointing to before the first element.
-     * @returns {ReverseIterator}
-     * @example
-     * let set = new TreeSet();
-     * ...
-     * for (let it = set.rbegin(); !it.equals(set.rend()); it.next()) {
-     *   console.log(`key: ${it.key}`);
-     * }
-     */
-    rend() {
-        return this.__t.rend();
-    }
-
-    /**
-     * Iterator pointing to the first element that is greater than key. If no such element is found end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let set = new TreeSet();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive
-     * let from = set.lowerBound(0);
-     * let to = set.upperBound(50);
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     *
-     * let set = new TreeSet();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
-     * let from = new ReverseIterator(set.upperBound(50));
-     * let to = new ReverseIterator(set.lowerBound(0));
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     */
-    upperBound(key) {
-        return this.__t.upperBound(key);
-    }
-
-    /**
-     * @returns first element of the container, or undefined if container is empty
-     * @example
-     * let set = new TreeSet([1, 2, 3]);
-     * let first = set.first(); // 1
-     */
-    first() {
-        return this.__t.first();
-    }
-
-    /**
-     * @returns last element of the container, or undefined if container is empty
-     * @example
-     * let set = new TreeSet([1, 2, 3]);
-     * let last = set.last(); // 3
-     */
-    last() {
-        return this.__t.last();
-    }
-
-    /**
-     * Serializes contents of the set in the form {key1,key2,...}
-     * @returns {String}
-     */
-    toString() {
-        return this.__t.toString();
-    }
-}
-
-module.exports = {
-    TreeSet: TreeSet,
-};
-
-/***/ }),
-/* 11 */
-/***/ (function(module, exports, __nested_webpack_require_96249__) {
-
-/** An implementation of red-black tree */
-const {Tree} = __nested_webpack_require_96249__(2);
-/** Classes that regulate whether tree nodes hold keys only, or key-value pairs */
-const {KeyOnlyPolicy} = __nested_webpack_require_96249__(1);
-/** Node for a red-black tree */
-const {TreeNode} = __nested_webpack_require_96249__(0);
-
-/**
- * TreeMultiSet is a container that stores elements following a specific order,
- * and where multiple elements can have equivalent values.
- *
- * In a TreeMultiSet, the value of an element also identifies it
- * (the value is itself the key). The value of the elements in a multiset
- * cannot be modified once in the container (the elements are always immutable),
- * but they can be inserted or removed from the container.
- *
- * ## Container properties
- * * **Associative** - Elements in associative containers are referenced
- * by their key and not by their absolute position in the container.
- * * **Ordered** - The elements in the container follow a strict order
- * at all times. All inserted elements are given a position in this order.
- * * **Set** - The value of an element is also the key used to identify it.
- * * **Multiple equivalent keys** - Multiple elements in the container
- * can have equivalent keys.
- *
- * @example
- * let set = new TreeMultiSet();
- * // add few values
- * set.add(1);
- * set.add(2);
- * set.add(2);
- * // check whether key exists
- * let flag = set.has(1); // << true
- * // print all keys
- * for (let key of set) {
- *   console.log(`key: ${key}`); // 1, 2, 2
- * }
- */
-class TreeMultiSet {
-    /*======================================================
-     * Methods of ES6 Set
-     *======================================================*/
-
-    /**
-     * Creates an empty, or a pre-initialized set.
-     * @param {*} [iterable] Another iterable object whose values are added into the newly created set.
-     * @example
-     * // Create an empty set
-     * let set = new TreeMultiSet();
-     * // Create and initialize set
-     * let set2 = new TreeMultiSet([1, 2, 3]);
-     */
-    constructor(iterable) {
-        /** Internal tree */
-        this.__t = new Tree();
-        this.__t.valuePolicy = new KeyOnlyPolicy();
-        if ((iterable !== undefined)
-            && (iterable !== null)) {
-            if (iterable[Symbol.iterator] !== undefined) {
-                // copy contents
-                for (let k of iterable) {
-                    this.add(k);
-                }
-            }
-            else {
-                throw new Error('TreeMultiSet constructor accepts only iterable objects');
-            }
-        }
-    }
-
-    /**
-     * String tag of this class
-     * @returns {String}
-     * @example
-     * Object.prototype.toString.call(new TreeMultiSet()); // "[object TreeMultiSet]"
-     */
-    get [Symbol.toStringTag]() {
-        return 'TreeMultiSet';
-    }
-
-    /**
-     * Allows to create programmatically an instance of the same class
-     * @returns constructor object for this class.
-     * @example
-     * let set = new TreeMultiSet();
-     * let constrFunc = Object.getPrototypeOf(set).constructor[Symbol.species];
-     * let set2 = new constrFunc();
-     */
-    static get [Symbol.species]() {
-        return TreeMultiSet;
-    }
-
-    /**
-     * Removes all key-value pairs.
-     * @example
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * set.clear();
-     * console.log(set.size); // 0
-     */
-    clear() {
-        this.__t.clear();
-    }
-
-    /**
-     * Removes key-value pair with the specified key if such entry exists. Does nothing otherwise.
-     * @example
-     * let set = new TreeMultiSet([1, 2, 2, 3]);
-     * set.delete(2);
-     * console.log(set.toString()); // {1,2,3}
-     * set.delete(2); / remove the second copy of the key
-     * console.log(set.toString()); // {1,3}
-     */
-    delete(key) {
-        let it = this.__t.find(key);
-        if (!it.equals(this.__t.end())) {
-            this.__t.erase(it.node);
-        }
-    }
-
-    /**
-     * Forward ES6 iterator for all values in ascending order.
-     * @returns {JsIterator}
-     * @example
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * for (let key of set.entries()) {
-     *   console.log(`key: ${key}`);
-     * }
-     */
-    entries() {
-        return this.__t.entries();
-    }
-
-    /**
-     * Iterates all values using a callback in ascending order.
-     * Note that ES6 specifies the order of key parameters in the callback differently from for-of loop.
-     * @example
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * set.forEach(function(value, key, container) {
-     *   // value is the same as key
-     *   console.log(`key: ${key}, value: ${value}`);
-     * });
-     */
-    forEach(callback) {
-        for (let k of this.__t) {
-            callback(k, k, this);
-        }
-    }
-
-    /**
-     * A boolean indicator whether set contains the specified key.
-     * @returns {Boolean}
-     * @param {*} key a value of any type that can be compared with a key
-     * @example
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * let b = set.get(3); // true
-     * b = set.get(4); // false
-     */
-    has(key) {
-        let it = this.__t.find(key);
-        if (!it.equals(this.__t.end())) {
-            return true;
-        }
-        else {
-            return false;
-        }
-    }
-
-    /**
-     * Forward ES6 iterator for all keys in ascending order.
-     * @returns {JsIterator}
-     * @example
-     * // iterate all keys
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * for (let k of set.keys()) {
-     *   console.log(k); // 1, 2, 3
-     * }
-     * // iterate all keys in reverse order
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * for (let k of set.keys().backward()) {
-     *   console.log(k); // 3, 2, 1
-     * }
-     */
-    keys() {
-        return this.__t.keys();
-    }
-
-    /**
-     * Adds a key to the set. If the key already exists then another entry with the same value is added.
-     * @param {*} key
-     * @example
-     * let set = new TreeMultiSet();
-     * set.add(1);
-     * set.add(1);
-     * set.add(2);
-     * // print all keys
-     * for (let key of set) {
-     *   console.log(`key: ${key}`); // 1, 1, 2
-     * }
-     */
-    add(key) {
-        let n = new TreeNode();
-        n.key = key;
-        this.__t.insertMulti(n);
-    }
-
-    /**
-     * Number of keys in the set.
-     * @returns {Number}
-     */
-    get size() {
-        return this.__t.size();
-    }
-
-    /**
-     * Forward ES6 iterator for all keys in ascending order. It is the same as keys() method
-     * @returns {JsITerator}
-     * @example
-     * // iterate all values
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * for (let v of set.values()) {
-     *   console.log(v); // '1', '2', '3'
-     * }
-     * // iterate all values in reverse order
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * for (let v of set.values().backward()) {
-     *   console.log(v); // '3', '2', '1'
-     * }
-     */
-    values() {
-        return this.__t.keys();
-    }
-
-    /**
-     * Forward ES6 iterator for all keys in ascending order. The same as entries() method
-     * @returns {JsIterator}
-     * @example
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * for (let key of set) {
-     *   console.log(`key: ${key}, value: ${value}`);
-     * }
-     */
-    [Symbol.iterator]() {
-        return this.__t[Symbol.iterator]();
-    }
-
-    /*======================================================
-     * More methods
-     *======================================================*/
-    /**
-     * ES6 reverse iterator for all keys in descending order.
-     * @returns {JsReverseIterator}
-     * @example
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * for (let key of set.backwards()) {
-     *   console.log(`key: ${key}`);
-     * }
-     */
-    backward() {
-        return this.__t.backward();
-    }
-
-    /**
-     * Sets custom comparison function if key values are not of primitive types.
-     * Callback is a 3-way comparison function accepts two key values (lhs, rhs). It is expected to return
-     *      +1 if the value of rhs is greater than lhs
-     *      -1 if the value of rhs is less than lhs
-     *       0 if values are the same
-     */
-    set compareFunc(func) {
-        this.clear();
-        this.__t.compare = func;
-    }
-
-    /*======================================================
-     * STL-like methods
-     *======================================================*/
-
-    /**
-     * Forward iterator to the first element
-     * @returns {Iterator}
-     * @example
-     * let set = new TreeMultiSet();
-     * ...
-     * for (let it = set.begin(); !it.equals(set.end()); it.next()) {
-     *   console.log(`key: ${it.key}`);
-     * }
-     */
-    begin() {
-        return this.__t.begin();
-    }
-
-    /**
-     * Forward iterator to the element following the last element
-     * @returns {Iterator}
-     * @example
-     * let set = new TreeMultiSet();
-     * ...
-     * for (let it = set.begin(); !it.equals(set.end()); it.next()) {
-     *   console.log(`key: ${it.key}`);
-     * }
-     */
-    end() {
-        return this.__t.end();
-    }
-
-    /**
-     * Finds an element with key equivalent to the specified one. If such key does not exist end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * ...
-     * let it = set.find(1);
-     * if (!it.equals(set.end())) {
-     *   console.log(`Found key: ${it.key}`); // 1
-     * }
-     */
-    find(key) {
-        return this.__t.find(key);
-    }
-
-    /**
-     * Adds key if such key does not exist in the set.
-     * @param {*} key
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     * @example
-     * let set = new TreeMultiSet();
-     * set.insertUnique(1);
-     * set.insertUnique(1); // this step has no effect on the set
-     * let flag = set.has(1); // true
-     * let size = set.size; // 1
-     */
-    insertUnique(key) {
-        let n = new TreeNode();
-        n.key = key;
-        return this.__t.insertUnique(n);
-    }
-
-    /**
-     * Adds key if such key does not exist in the set. Same as insertUnique()
-     * @param {*} key
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     * @example
-     * let set = new TreeMultiSet();
-     * set.insertOrReplace(1);
-     * set.insertOrReplace(1); // this step has no effect on the set
-     * let flag = set.has(1); // true
-     * let size = set.size; // 1
-     */
-    insertOrReplace(key) {
-        let n = new TreeNode();
-        n.key = key;
-        return this.__t.insertOrReplace(n);
-    }
-
-    /**
-     * Adds key whether it exists or not in the set.
-     * @param {*} key
-     * @returns {InsertionResult} - indicates whether a node was added and provides iterator to it.
-     * @example
-     * let set = new TreeMultiSet();
-     * set.insertMulti(1);
-     * set.insertMulti(1); // this step has no effect on the map
-     * let flag = set.has(1); // true
-     * let size = set.size; // 2
-     */
-    insertMulti(key) {
-        let n = new TreeNode();
-        n.key = key;
-        return this.__t.insertMulti(n);
-    }
-
-    /**
-     * Removes value for the specified iterator.
-     * @param {Iterator} iterator
-     * @example
-     * let set = new TreeMultiSet([1,2,3]);
-     * let it = set.find(2);
-     * it.prev();
-     * set.erase(it); // removes a node with key 1
-     * console.log(set.toString()); // {2,3}
-     */
-    erase(iterator) {
-        this.__t.erase(iterator.node);
-    }
-
-    /**
-     * Iterator pointing to the first element that is not less than specified key. If no such element is found, see end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let set = new TreeMultiSet();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive
-     * let from = set.lowerBound(0);
-     * let to = set.upperBound(50);
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     *
-     * let set = new TreeMultiSet();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
-     * let from = new ReverseIterator(set.upperBound(50));
-     * let to = new ReverseIterator(set.lowerBound(0));
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     */
-    lowerBound(key) {
-        return this.__t.lowerBound(key);
-    }
-
-    /**
-     * Reverse iterator to the last element.
-     * @returns {ReverseIterator}
-     * @example
-     * let set = new TreeMultiSet();
-     * ...
-     * for (let it = set.rbegin(); !it.equals(set.rend()); it.next()) {
-     *   console.log(`key: ${it.key}`);
-     * }
-     */
-    rbegin() {
-        return this.__t.rbegin();
-    }
-
-    /**
-     * Reverse iterator pointing to before the first element.
-     * @returns {ReverseIterator}
-     * @example
-     * let set = new TreeMultiSet();
-     * ...
-     * for (let it = set.rbegin(); !it.equals(set.rend()); it.next()) {
-     *   console.log(`key: ${it.key}`);
-     * }
-     */
-    rend() {
-        return this.__t.rend();
-    }
-
-    /**
-     * Iterator pointing to the first element that is greater than key. If no such element is found end() iterator is returned.
-     * @param {*} key
-     * @returns {Iterator}
-     * @example
-     * let set = new TreeMultiSet();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive
-     * let from = set.lowerBound(0);
-     * let to = set.upperBound(50);
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     *
-     * let set = new TreeMultiSet();
-     * ... // add key-value pairs., using numbers as keys
-     * // iterate through all key-value pairs with keys between 0 and 50 inclusive in reverse order
-     * let from = new ReverseIterator(set.upperBound(50));
-     * let to = new ReverseIterator(set.lowerBound(0));
-     * let it = from;
-     * while (!it.equals(to)) {
-     *   console.log(it.key);
-     *   it.next();
-     * }
-     */
-    upperBound(key) {
-        return this.__t.upperBound(key);
-    }
-
-    /**
-     * @returns first element of the container, or undefined if container is empty
-     * @example
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * let first = set.first(); // 1
-     */
-    first() {
-        return this.__t.first();
-    }
-
-    /**
-     * @returns last element of the container, or undefined if container is empty
-     * @example
-     * let set = new TreeMultiSet([1, 2, 3]);
-     * let last = set.last(); // 3
-     */
-    last() {
-        return this.__t.last();
-    }
-
-    /**
-     * Serializes contents of the set in the form {key1,key2,...}
-     * @returns {String}
-     */
-    toString() {
-        return this.__t.toString();
-    }
-}
-
-module.exports = {
-    TreeMultiSet: TreeMultiSet,
-};
-
-/***/ })
-/******/ ]);
-});
-
-/***/ }),
-
 /***/ 742:
 /***/ (function(module, __unusedexports, __webpack_require__) {
 
diff --git a/src/main.ts b/src/main.ts
index 71da8d9..19b00b3 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,9 +1,11 @@
 import * as github from '@actions/github'
 import * as core from '@actions/core'
 import * as rest from '@octokit/rest'
-import * as treemap from 'jstreemap'
 
-const CANCELLABLE_RUNS = [
+/**
+ Those are the cancellable event types tht we know about
+ */
+const CANCELLABLE_EVENT_TYPES = [
   'push',
   'pull_request',
   'workflow_run',
@@ -11,104 +13,190 @@ const CANCELLABLE_RUNS = [
   'workflow_dispatch'
 ]
 
+/**
+ * Those are different modes for cancelling
+ */
 enum CancelMode {
   DUPLICATES = 'duplicates',
+  ALL_DUPLICATES = 'allDuplicates',
   SELF = 'self',
   FAILED_JOBS = 'failedJobs',
   NAMED_JOBS = 'namedJobs'
 }
 
-function createListRunsQueryOtherRuns(
-  octokit: github.GitHub,
-  owner: string,
-  repo: string,
-  status: string,
-  workflowId: number | string,
-  headBranch: string,
+/**
+ * Stores information uniquely identifying the run by it's triggering source:
+ * It is the workflow id, Head Repo and Branch the change originates from and event name that originates it.
+ *
+ * All Runs coming from the same Pull Request share the same Source Group. Also all pushes to master
+ * share the same Source Group
+ */
+interface WorkflowRunSourceGroup {
+  workflowId: number | string
+  headRepo: string
+  headBranch: string
+  eventName: string
+}
+
+/**
+ * Stores information about the owner and repository used, as well as octokit object that is used for
+ * authentication.
+ */
+interface RepositoryInfo {
+  octokit: github.GitHub
+  owner: string
+  repo: string
+}
+
+/**
+ * Stores information about the workflow info that triggered the current workflow.
+ */
+interface TriggeringWorkflowInfo {
+  headRepo: string
+  headBranch: string
+  headSha: string
   eventName: string
+  mergeCommitSha: string | null
+  pullRequest: rest.PullsListResponseItem | null
+}
+
+/**
+ * Converts the source of a run object into a string that can be used as map key in maps where we keep
+ * arrays of runs per source group
+ * @param sourceGroup the object identifying the source of the run (Pull Request, Master Push)
+ * @returns the unique string id for the source group
+ */
+function getSourceGroupId(sourceGroup: WorkflowRunSourceGroup): string {
+  return `:${sourceGroup.workflowId}:${sourceGroup.headRepo}:${sourceGroup.headBranch}:${sourceGroup.eventName}`
+}
+
+/**
+ * Creates query parameters selecting all runs that share the same source group as we have. This can
+ * be used to select duplicates of my own run.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param mySourceGroup - source group of the originating run
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
+function createListRunsQueryRunsSameSource(
+  repositoryInfo: RepositoryInfo,
+  status: string,
+  mySourceGroup: WorkflowRunSourceGroup
 ): rest.RequestOptions {
   const request = {
-    owner,
-    repo,
+    owner: repositoryInfo.owner,
+    repo: repositoryInfo.repo,
     // eslint-disable-next-line @typescript-eslint/camelcase
-    workflow_id: workflowId,
+    workflow_id: mySourceGroup.workflowId,
     status,
-    branch: headBranch,
-    event: eventName
+    branch: mySourceGroup.headBranch,
+    event: mySourceGroup.eventName
   }
-  return octokit.actions.listWorkflowRuns.endpoint.merge(request)
+  return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request)
 }
-
-function createListRunsQueryMyOwnRun(
-  octokit: github.GitHub,
-  owner: string,
-  repo: string,
+/**
+ * Creates query parameters selecting only specific run Id.
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param workflowId - Id of the workflow to retrieve
+ * @param runId - run Id to retrieve
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
+function createListRunsQuerySpecificRunId(
+  repositoryInfo: RepositoryInfo,
   status: string,
   workflowId: number | string,
   runId: number
 ): rest.RequestOptions {
   const request = {
-    owner,
-    repo,
+    owner: repositoryInfo.owner,
+    repo: repositoryInfo.repo,
     // eslint-disable-next-line @typescript-eslint/camelcase
     workflow_id: workflowId,
     status,
     // eslint-disable-next-line @typescript-eslint/camelcase
     run_id: runId.toString()
   }
-  return octokit.actions.listWorkflowRuns.endpoint.merge(request)
+  return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request)
 }
 
+/**
+ * Creates query parameters selecting all run Ids for specified workflow Id.
+ * @param repositoryInfo - information about the repository used
+ * @param status - status of the run that we are querying for
+ * @param workflowId - Id of the workflow to retrieve
+ * @return query parameters merged with the listWorkflowRuns criteria
+ */
 function createListRunsQueryAllRuns(
-  octokit: github.GitHub,
-  owner: string,
-  repo: string,
+  repositoryInfo: RepositoryInfo,
   status: string,
   workflowId: number | string
 ): rest.RequestOptions {
   const request = {
-    owner,
-    repo,
+    owner: repositoryInfo.owner,
+    repo: repositoryInfo.repo,
     // eslint-disable-next-line @typescript-eslint/camelcase
     workflow_id: workflowId,
     status
   }
-  return octokit.actions.listWorkflowRuns.endpoint.merge(request)
+  return repositoryInfo.octokit.actions.listWorkflowRuns.endpoint.merge(request)
 }
 
+/**
+ * Creates query parameters selecting all jobs for specified run Id.
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @return query parameters merged with the listJobsForWorkflowRun criteria
+ */
 function createJobsForWorkflowRunQuery(
-  octokit: github.GitHub,
-  owner: string,
-  repo: string,
+  repositoryInfo: RepositoryInfo,
   runId: number
 ): rest.RequestOptions {
   const request = {
-    owner,
-    repo,
+    owner: repositoryInfo.owner,
+    repo: repositoryInfo.repo,
     // eslint-disable-next-line @typescript-eslint/camelcase
     run_id: runId
   }
-  return octokit.actions.listJobsForWorkflowRun.endpoint.merge(request)
+  return repositoryInfo.octokit.actions.listJobsForWorkflowRun.endpoint.merge(
+    request
+  )
 }
 
-function matchInArray(s: string, regexps: string[]): boolean {
+/**
+ * Returns true if the string matches any of the regexps in array of regexps
+ * @param stringToMatch string to match
+ * @param regexps array of regexp to match the string against
+ * @return true if there is a match
+ */
+function matchInArray(stringToMatch: string, regexps: string[]): boolean {
   for (const regexp of regexps) {
-    if (s.match(regexp)) {
+    if (stringToMatch.match(regexp)) {
       return true
     }
   }
   return false
 }
 
+/**
+ * Returns true if the runId specified has jobs matching the regexp and optionally checks if those
+ * jobs are failed.
+ * * If checkIfFailed is False, it returns true if any of the job name for the run match any of the regexps
+ * * Id checkIfFailed is True, it returns true if any of the matching jobs have status 'failed'
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @param jobNameRegexps - array of job name regexps
+ * @param checkIfFailed - whether to check the 'failed' status of matched jobs
+ * @return true if there is a match
+ */
 async function jobsMatchingNames(
-  octokit: github.GitHub,
-  owner: string,
-  repo: string,
+  repositoryInfo: RepositoryInfo,
   runId: number,
   jobNameRegexps: string[],
   checkIfFailed: boolean
 ): Promise<boolean> {
-  const listJobs = createJobsForWorkflowRunQuery(octokit, owner, repo, runId)
+  const listJobs = createJobsForWorkflowRunQuery(repositoryInfo, runId)
   if (checkIfFailed) {
     core.info(
       `\nChecking if runId ${runId} has job names matching any of the ${jobNameRegexps} that failed\n`
@@ -118,7 +206,7 @@ async function jobsMatchingNames(
       `\nChecking if runId ${runId} has job names matching any of the ${jobNameRegexps}\n`
     )
   }
-  for await (const item of octokit.paginate.iterator(listJobs)) {
+  for await (const item of repositoryInfo.octokit.paginate.iterator(listJobs)) {
     for (const job of item.data.jobs) {
       core.info(`    The job name: ${job.name}, Conclusion: ${job.conclusion}`)
       if (matchInArray(job.name, jobNameRegexps)) {
@@ -126,12 +214,14 @@ async function jobsMatchingNames(
           // Only fail the build if one of the matching jobs fail
           if (job.conclusion === 'failure') {
             core.info(
-              `    The Job ${job.name} matches one of the ${jobNameRegexps} regexps and it failed. Cancelling run.`
+              `    The Job ${job.name} matches one of the ${jobNameRegexps} regexps and it failed.` +
+                ` Cancelling run.`
             )
             return true
           } else {
             core.info(
-              `    The Job ${job.name} matches one of the ${jobNameRegexps} regexps but it did not fail. So far, so good.`
+              `    The Job ${job.name} matches one of the ${jobNameRegexps} regexps but it did not fail. ` +
+                ` So far, so good.`
             )
           }
         } else {
@@ -147,41 +237,62 @@ async function jobsMatchingNames(
   return false
 }
 
+/**
+ * Retrieves workflowId from the workflow URL.
+ * @param workflowUrl workflow URL to retrieve the ID from
+ * @return numerical workflow id
+ */
+function retrieveWorkflowIdFromUrl(workflowUrl: string): number {
+  const workflowIdString = workflowUrl.split('/').pop() || ''
+  if (!(workflowIdString.length > 0)) {
+    throw new Error('Could not resolve workflow')
+  }
+  return parseInt(workflowIdString)
+}
+
+/**
+ * Returns workflowId of the runId specified
+ * @param repositoryInfo - information about the repository used
+ * @param runId - Id of the run to retrieve jobs for
+ * @return workflow ID for the run id
+ */
 async function getWorkflowId(
-  octokit: github.GitHub,
-  runId: number,
-  owner: string,
-  repo: string
+  repositoryInfo: RepositoryInfo,
+  runId: number
 ): Promise<number> {
-  const reply = await octokit.actions.getWorkflowRun({
-    owner,
-    repo,
+  const reply = await repositoryInfo.octokit.actions.getWorkflowRun({
+    owner: repositoryInfo.owner,
+    repo: repositoryInfo.repo,
     // eslint-disable-next-line @typescript-eslint/camelcase
     run_id: runId
   })
   core.info(`The source run ${runId} is in ${reply.data.workflow_url} workflow`)
-  const workflowIdString = reply.data.workflow_url.split('/').pop() || ''
-  if (!(workflowIdString.length > 0)) {
-    throw new Error('Could not resolve workflow')
-  }
-  return parseInt(workflowIdString)
+  return retrieveWorkflowIdFromUrl(reply.data.workflow_url)
 }
 
+/**
+ * Returns workflow runs matching the callable adding query criteria
+ * @param repositoryInfo - information about the repository used
+ * @param statusValues - array of string status values for runs that we are interested at
+ * @param cancelMode - which cancel mode the query is about
+ * @param createListRunQuery - what is the callable criteria selection
+ * @return map of workflow run items indexed by the workflow run number
+ */
 async function getWorkflowRuns(
-  octokit: github.GitHub,
+  repositoryInfo: RepositoryInfo,
   statusValues: string[],
   cancelMode: CancelMode,
   createListRunQuery: CallableFunction
-): Promise<
-  treemap.TreeMap<number, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem>
-> {
-  const workflowRuns = new treemap.TreeMap<
+): Promise<Map<number, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem>> {
+  const workflowRuns = new Map<
     number,
     rest.ActionsListWorkflowRunsResponseWorkflowRunsItem
   >()
   for (const status of statusValues) {
     const listRuns = await createListRunQuery(status)
-    for await (const item of octokit.paginate.iterator(listRuns)) {
+    for await (const item of repositoryInfo.octokit.paginate.iterator(
+      listRuns
+    )) {
       // There is some sort of bug where the pagination URLs point to a
       // different endpoint URL which trips up the resulting representation
       // In that case, fallback to the actual REST 'workflow_runs' property
@@ -196,10 +307,134 @@ async function getWorkflowRuns(
   return workflowRuns
 }
 
-async function shouldBeCancelled(
-  octokit: github.GitHub,
-  owner: string,
-  repo: string,
+/**
+ * True if the request is candidate for cancelling in case of duplicate deletion
+ * @param runItem item to check
+ * @param headRepo head Repo that we are checking against
+ * @param cancelFutureDuplicates whether future duplicates are being cancelled
+ * @param sourceRunId the source Run Id that originates the request
+ * @return true if we determine that the run Id should be cancelled
+ */
+function isCandidateForCancellingDuplicate(
+  runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
+  headRepo: string,
+  cancelFutureDuplicates: boolean,
+  sourceRunId: number
+): boolean {
+  const runHeadRepo = runItem.head_repository.full_name
+  if (headRepo !== undefined && runHeadRepo !== headRepo) {
+    core.info(
+      `\nThe run ${runItem.id} is from a different ` +
+        `repo: ${runHeadRepo} (expected ${headRepo}). Not cancelling it\n`
+    )
+    return false
+  }
+  if (cancelFutureDuplicates) {
+    core.info(
+      `\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`
+    )
+    return true
+  } else {
+    if (runItem.id === sourceRunId) {
+      core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`)
+      return false
+    } else if (runItem.id > sourceRunId) {
+      core.info(
+        `\nThe run ${runItem.id} is started later than my own run ${sourceRunId}. Not returning it\n`
+      )
+      return false
+    }
+    core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`)
+    return true
+  }
+}
+
+/**
+ * Should the run is candidate for cancelling in SELF cancelling mode?
+ * @param runItem run item
+ * @param sourceRunId source run id
+ * @return true if the run item is self
+ */
+function isCandidateForCancellingSelf(
+  runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
+  sourceRunId: number
+): boolean {
+  if (runItem.id === sourceRunId) {
+    core.info(`\nReturning the "source" run: ${runItem.id}.\n`)
+    return true
+  } else {
+    return false
+  }
+}
+
+/**
+ * Should the run is candidate for cancelling in naming job cancelling mode?
+ * @param repositoryInfo - information about the repository used
+ * @param runItem run item
+ * @param jobNamesRegexps array of regexps to match job names against
+ * @return true if the run item contains jobs with names matching the pattern
+ */
+async function isCandidateForCancellingNamedJobs(
+  repositoryInfo: RepositoryInfo,
+  runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
+  jobNamesRegexps: string[]
+): Promise<boolean> {
+  // Cancel all jobs that have failed jobs (no matter when started)
+  if (
+    await jobsMatchingNames(repositoryInfo, runItem.id, jobNamesRegexps, false)
+  ) {
+    core.info(
+      `\nSome jobs have matching names in ${runItem.id} . Returning it.\n`
+    )
+    return true
+  } else {
+    core.info(`\nNone of the jobs match name in ${runItem.id}. Returning it.\n`)
+    return false
+  }
+}
+
+/**
+ * Should the run is candidate for cancelling in failed job cancelling mode?
+ * @param repositoryInfo - information about the repository used
+ * @param runItem run item
+ * @param jobNamesRegexps array of regexps to match job names against
+ * @return true if the run item contains failed jobs with names matching the pattern
+ */
+async function isCandidateForCancellingFailedJobs(
+  repositoryInfo: RepositoryInfo,
+  runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
+  jobNamesRegexps: string[]
+): Promise<boolean> {
+  // Cancel all jobs that have failed jobs (no matter when started)
+  if (
+    await jobsMatchingNames(repositoryInfo, runItem.id, jobNamesRegexps, true)
+  ) {
+    core.info(
+      `\nSome matching named jobs failed in ${runItem.id} . Cancelling it.\n`
+    )
+    return true
+  } else {
+    core.info(
+      `\nNone of the matching jobs failed in ${runItem.id}. Not cancelling it.\n`
+    )
+    return false
+  }
+}
+
+/**
+ * Determines whether the run is candidate to be cancelled depending on the mode used
+ * @param repositoryInfo - information about the repository used
+ * @param runItem - run item
+ * @param headRepo - head repository
+ * @param cancelMode - cancel mode
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param sourceRunId - what is the source run id
+ * @param jobNamesRegexps - what are the regexps for job names
+ * @param skipEventTypes - which events should be skipped
+ * @return true if the run id is candidate for cancelling
+ */
+async function isCandidateForCancelling(
+  repositoryInfo: RepositoryInfo,
   runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem,
   headRepo: string,
   cancelMode: CancelMode,
@@ -212,9 +447,10 @@ async function shouldBeCancelled(
     core.info(`\nThe run ${runItem.id} is completed. Not cancelling it.\n`)
     return false
   }
-  if (!CANCELLABLE_RUNS.includes(runItem.event.toString())) {
+  if (!CANCELLABLE_EVENT_TYPES.includes(runItem.event.toString())) {
     core.info(
-      `\nThe run ${runItem.id} is (${runItem.event} event - not in ${CANCELLABLE_RUNS}). Not cancelling it.\n`
+      `\nThe run ${runItem.id} is (${runItem.event} event - not ` +
+        `in ${CANCELLABLE_EVENT_TYPES}). Not cancelling it.\n`
     )
     return false
   }
@@ -226,83 +462,31 @@ async function shouldBeCancelled(
     return false
   }
   if (cancelMode === CancelMode.FAILED_JOBS) {
-    // Cancel all jobs that have failed jobs (no matter when started)
-    if (
-      await jobsMatchingNames(
-        octokit,
-        owner,
-        repo,
-        runItem.id,
-        jobNamesRegexps,
-        true
-      )
-    ) {
-      core.info(
-        `\nSome matching named jobs failed in ${runItem.id} . Cancelling it.\n`
-      )
-      return true
-    } else {
-      core.info(
-        `\nNone of the matching jobs failed in ${runItem.id}. Not cancelling it.\n`
-      )
-      return false
-    }
+    return await isCandidateForCancellingFailedJobs(
+      repositoryInfo,
+      runItem,
+      jobNamesRegexps
+    )
   } else if (cancelMode === CancelMode.NAMED_JOBS) {
-    // Cancel all jobs that have failed jobs (no matter when started)
-    if (
-      await jobsMatchingNames(
-        octokit,
-        owner,
-        repo,
-        runItem.id,
-        jobNamesRegexps,
-        false
-      )
-    ) {
-      core.info(
-        `\nSome jobs have matching names in ${runItem.id} . Returning it.\n`
-      )
-      return true
-    } else {
-      core.info(
-        `\nNone of the jobs match name in ${runItem.id}. Returning it.\n`
-      )
-      return false
-    }
+    return await isCandidateForCancellingNamedJobs(
+      repositoryInfo,
+      runItem,
+      jobNamesRegexps
+    )
   } else if (cancelMode === CancelMode.SELF) {
-    if (runItem.id === sourceRunId) {
-      core.info(`\nReturning the "source" run: ${runItem.id}.\n`)
-      return true
-    } else {
-      return false
-    }
+    return isCandidateForCancellingSelf(runItem, sourceRunId)
   } else if (cancelMode === CancelMode.DUPLICATES) {
-    const runHeadRepo = runItem.head_repository.full_name
-    if (headRepo !== undefined && runHeadRepo !== headRepo) {
-      core.info(
-        `\nThe run ${runItem.id} is from a different ` +
-          `repo: ${runHeadRepo} (expected ${headRepo}). Not cancelling it\n`
-      )
-      return false
-    }
-    if (cancelFutureDuplicates) {
-      core.info(
-        `\nCancel Future Duplicates: Returning run id that might be duplicate or my own run: ${runItem.id}.\n`
-      )
-      return true
-    } else {
-      if (runItem.id === sourceRunId) {
-        core.info(`\nThis is my own run ${runItem.id}. Not returning myself!\n`)
-        return false
-      } else if (runItem.id > sourceRunId) {
-        core.info(
-          `\nThe run ${runItem.id} is started later than my own run ${sourceRunId}. Not returning it\n`
-        )
-        return false
-      }
-      core.info(`\nFound duplicate of my own run: ${runItem.id}.\n`)
-      return true
-    }
+    return isCandidateForCancellingDuplicate(
+      runItem,
+      headRepo,
+      cancelFutureDuplicates,
+      sourceRunId
+    )
+  } else if (cancelMode === CancelMode.ALL_DUPLICATES) {
+    core.info(
+      `Returning candidate ${runItem.id} for "all_duplicates" cancelling.`
+    )
+    return true
   } else {
     throw Error(
       `\nWrong cancel mode ${cancelMode}! This should never happen.\n`
@@ -310,17 +494,20 @@ async function shouldBeCancelled(
   }
 }
 
+/**
+ * Cancels the specified workflow run.
+ * @param repositoryInfo - information about the repository used
+ * @param runId - run Id to cancel
+ */
 async function cancelRun(
-  octokit: github.GitHub,
-  owner: string,
-  repo: string,
+  repositoryInfo: RepositoryInfo,
   runId: number
 ): Promise<void> {
   let reply
   try {
-    reply = await octokit.actions.cancelWorkflowRun({
-      owner,
-      repo,
+    reply = await repositoryInfo.octokit.actions.cancelWorkflowRun({
+      owner: repositoryInfo.owner,
+      repo: repositoryInfo.repo,
       // eslint-disable-next-line @typescript-eslint/camelcase
       run_id: runId
     })
@@ -332,72 +519,71 @@ async function cancelRun(
   }
 }
 
-async function findAndCancelRuns(
-  octokit: github.GitHub,
-  selfRunId: number,
+/**
+ * Returns map of workflow run items matching the criteria specified group by workflow run id
+ * @param repositoryInfo - information about the repository used
+ * @param statusValues - status values we want to check
+ * @param cancelMode - cancel mode to use
+ * @param sourceWorkflowId - source workflow id
+ * @param sourceRunId - source run id
+ * @param sourceEventName - name of the source event
+ * @param mySourceGroup - source of the run that originated it
+ * @return map of the run items matching grouped by workflow run id
+ */
+async function getWorkflowRunsMatchingCriteria(
+  repositoryInfo: RepositoryInfo,
+  statusValues: string[],
+  cancelMode:
+    | CancelMode
+    | CancelMode.DUPLICATES
+    | CancelMode.ALL_DUPLICATES
+    | CancelMode.FAILED_JOBS
+    | CancelMode.NAMED_JOBS,
   sourceWorkflowId: number | string,
   sourceRunId: number,
-  owner: string,
-  repo: string,
-  headRepo: string,
-  headBranch: string,
   sourceEventName: string,
-  cancelMode: CancelMode,
-  cancelFutureDuplicates: boolean,
-  notifyPRCancel: boolean,
-  notifyPRMessageStart: string,
-  jobNameRegexps: string[],
-  skipEventTypes: string[],
-  reason: string
-): Promise<number[]> {
-  const statusValues = ['queued', 'in_progress']
-  const workflowRuns = await getWorkflowRuns(
-    octokit,
+  mySourceGroup: WorkflowRunSourceGroup
+): Promise<Map<number, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem>> {
+  return await getWorkflowRuns(
+    repositoryInfo,
     statusValues,
     cancelMode,
     function(status: string) {
       if (cancelMode === CancelMode.SELF) {
         core.info(
-          `\nFinding runs for my own run: Owner: ${owner}, Repo: ${repo}, ` +
+          `\nFinding runs for my own run: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
             `Workflow ID:${sourceWorkflowId}, Source Run id: ${sourceRunId}\n`
         )
-        return createListRunsQueryMyOwnRun(
-          octokit,
-          owner,
-          repo,
+        return createListRunsQuerySpecificRunId(
+          repositoryInfo,
           status,
           sourceWorkflowId,
           sourceRunId
         )
       } else if (
         cancelMode === CancelMode.FAILED_JOBS ||
-        cancelMode === CancelMode.NAMED_JOBS
+        cancelMode === CancelMode.NAMED_JOBS ||
+        cancelMode === CancelMode.ALL_DUPLICATES
       ) {
         core.info(
-          `\nFinding runs for all runs: Owner: ${owner}, Repo: ${repo}, Status: ${status} ` +
-            `Workflow ID:${sourceWorkflowId}\n`
+          `\nFinding runs for all runs: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
+            `Status: ${status} Workflow ID:${sourceWorkflowId}\n`
         )
         return createListRunsQueryAllRuns(
-          octokit,
-          owner,
-          repo,
+          repositoryInfo,
           status,
           sourceWorkflowId
         )
       } else if (cancelMode === CancelMode.DUPLICATES) {
         core.info(
-          `\nFinding duplicate runs: Owner: ${owner}, Repo: ${repo}, Status: ${status} ` +
-            `Workflow ID:${sourceWorkflowId}, Head Branch: ${headBranch},` +
+          `\nFinding duplicate runs: Owner: ${repositoryInfo.owner}, Repo: ${repositoryInfo.repo}, ` +
+            `Status: ${status} Workflow ID:${sourceWorkflowId}, Head Branch: ${mySourceGroup.headBranch},` +
             `Event name: ${sourceEventName}\n`
         )
-        return createListRunsQueryOtherRuns(
-          octokit,
-          owner,
-          repo,
+        return createListRunsQueryRunsSameSource(
+          repositoryInfo,
           status,
-          sourceWorkflowId,
-          headBranch,
-          sourceEventName
+          mySourceGroup
         )
       } else {
         throw Error(
@@ -406,18 +592,105 @@ async function findAndCancelRuns(
       }
     }
   )
-  const workflowsToCancel: [number, string][] = []
-  const pullRequestToNotify: number[] = []
+}
+
+/**
+ * Finds pull request matching its headRepo, headBranch and headSha
+ * @param repositoryInfo - information about the repository used
+ * @param headRepo - head repository from which Pull Request comes
+ * @param headBranch - head branch from which Pull Request comes
+ * @param headSha - sha for the head of the incoming Pull request
+ */
+async function findPullRequest(
+  repositoryInfo: RepositoryInfo,
+  headRepo: string,
+  headBranch: string,
+  headSha: string
+): Promise<rest.PullsListResponseItem | null> {
+  // Finds Pull request for this workflow run
+  core.info(
+    `\nFinding PR request id for: owner: ${repositoryInfo.owner}, Repo:${repositoryInfo.repo},` +
+      ` Head:${headRepo}:${headBranch}.\n`
+  )
+  const pullRequests = await repositoryInfo.octokit.pulls.list({
+    owner: repositoryInfo.owner,
+    repo: repositoryInfo.repo,
+    head: `${headRepo}:${headBranch}`
+  })
+  for (const pullRequest of pullRequests.data) {
+    core.info(
+      `\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`
+    )
+    if (pullRequest.head.sha === headSha) {
+      core.info(`\nFound PR: ${pullRequest.number}\n`)
+      return pullRequest
+    }
+  }
+  core.info(`\nCould not find the PR for this build :(\n`)
+  return null
+}
+
+/**
+ * Finds pull request id for the run item.
+ * @param repositoryInfo - information about the repository used
+ * @param runItem - run Item that the pull request should be found for
+ * @return pull request number to notify (or undefined if not found)
+ */
+async function findPullRequestForRunItem(
+  repositoryInfo: RepositoryInfo,
+  runItem: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem
+): Promise<number | undefined> {
+  const pullRequest = await findPullRequest(
+    repositoryInfo,
+    runItem.head_repository.owner.login,
+    runItem.head_branch,
+    runItem.head_sha
+  )
+  if (pullRequest) {
+    return pullRequest.number
+  }
+  return undefined
+}
+
+/**
+ * Maps found workflow runs into groups - filters out the workflows that are not eligible for canceling
+ * (depends on cancel Mode) and assigns each workflow to groups - where workflow runs from the
+ * same group are put together in one array - in a map indexed by the source group id.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param headRepo - head repository the event comes from
+ * @param cancelMode - cancel mode to use
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param sourceRunId - source run id for the run
+ * @param jobNameRegexps - regexps for job names
+ * @param skipEventTypes - array of event names to skip
+ * @param workflowRuns - map of workflow runs found
+ * @parm map where key is the source group id and value is array of workflow run item candidates to cancel
+ */
+async function filterAndMapWorkflowRunsToGroups(
+  repositoryInfo: RepositoryInfo,
+  headRepo: string,
+  cancelMode: CancelMode,
+  cancelFutureDuplicates: boolean,
+  sourceRunId: number,
+  jobNameRegexps: string[],
+  skipEventTypes: string[],
+  workflowRuns: Map<
+    number,
+    rest.ActionsListWorkflowRunsResponseWorkflowRunsItem
+  >
+): Promise<
+  Map<string, rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]>
+> {
+  const mapOfWorkflowRunCandidates = new Map()
   for (const [key, runItem] of workflowRuns) {
     core.info(
       `\nChecking run number: ${key}, RunId: ${runItem.id}, Url: ${runItem.url}. Status ${runItem.status},` +
         ` Created at ${runItem.created_at}\n`
     )
     if (
-      await shouldBeCancelled(
-        octokit,
-        owner,
-        repo,
+      await isCandidateForCancelling(
+        repositoryInfo,
         runItem,
         headRepo,
         cancelMode,
@@ -427,135 +700,287 @@ async function findAndCancelRuns(
         skipEventTypes
       )
     ) {
-      if (notifyPRCancel && runItem.event === 'pull_request') {
-        const pullRequest = await findPullRequest(
-          octokit,
-          owner,
-          repo,
-          runItem.head_repository.owner.login,
-          runItem.head_branch,
-          runItem.head_sha
-        )
-        if (pullRequest) {
-          pullRequestToNotify.push(pullRequest.number)
-        }
+      const candidateSourceGroup: WorkflowRunSourceGroup = {
+        workflowId: retrieveWorkflowIdFromUrl(runItem.workflow_url),
+        headBranch: runItem.head_branch,
+        headRepo: runItem.head_repository.full_name,
+        eventName: runItem.event
+      }
+      const sourceGroupId = getSourceGroupId(candidateSourceGroup)
+      let workflowRunArray:
+        | rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
+        | undefined = mapOfWorkflowRunCandidates.get(sourceGroupId)
+      if (workflowRunArray === undefined) {
+        workflowRunArray = []
+        mapOfWorkflowRunCandidates.set(sourceGroupId, workflowRunArray)
       }
-      workflowsToCancel.push([runItem.id, runItem.created_at])
-    }
-  }
-  // Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
-  const sortedRunTuplesToCancel = workflowsToCancel.sort(
-    (runTuple1, runTuple2) => runTuple2[1].localeCompare(runTuple1[1])
-  )
-  if (sortedRunTuplesToCancel.length > 0) {
-    if (cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) {
       core.info(
-        `\nSkipping the first run (${sortedRunTuplesToCancel[0]}) of all the matching ` +
-          `duplicates - this one we are going to leave in peace!\n`
+        `The candidate ${runItem.id} has been added to ${sourceGroupId} group of candidates`
       )
-      sortedRunTuplesToCancel.shift()
-    }
-    if (sortedRunTuplesToCancel.length === 0) {
-      core.info(`\nNo duplicates to cancel!\n`)
-      return sortedRunTuplesToCancel.map(runTuple => runTuple[0])
+      workflowRunArray.push(runItem)
     }
-    core.info(
-      '\n######  Cancelling runs starting from the oldest  ##########\n' +
-        `\n     Runs to cancel: ${sortedRunTuplesToCancel.length}\n` +
-        `\n     PRs to notify: ${pullRequestToNotify.length}\n`
-    )
-    for (const runTuple of sortedRunTuplesToCancel) {
-      core.info(`\nCancelling run: ${runTuple}.\n`)
-      await cancelRun(octokit, owner, repo, runTuple[0])
-    }
-    for (const pullRequestNumber of pullRequestToNotify) {
-      const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`
-      await addCommentToPullRequest(
-        octokit,
-        owner,
-        repo,
-        pullRequestNumber,
-        `[The Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`
-      )
-    }
-    core.info(
-      '\n######  Finished cancelling runs                  ##########\n'
-    )
-  } else {
-    core.info(
-      '\n######  There are no runs to cancel!              ##########\n'
-    )
   }
-  return sortedRunTuplesToCancel.map(runTuple => runTuple[0])
-}
-
-function getRequiredEnv(key: string): string {
-  const value = process.env[key]
-  if (value === undefined) {
-    const message = `${key} was not defined.`
-    throw new Error(message)
-  }
-  return value
+  return mapOfWorkflowRunCandidates
 }
 
+/**
+ * Add specified comment to Pull Request
+ * @param repositoryInfo - information about the repository used
+ * @param pullRequestNumber - number of pull request
+ * @param comment - comment to add
+ */
 async function addCommentToPullRequest(
-  octokit: github.GitHub,
-  owner: string,
-  repo: string,
+  repositoryInfo: RepositoryInfo,
   pullRequestNumber: number,
   comment: string
 ): Promise<void> {
   core.info(`\nNotifying PR: ${pullRequestNumber} with '${comment}'.\n`)
-  await octokit.issues.createComment({
-    owner,
-    repo,
+  await repositoryInfo.octokit.issues.createComment({
+    owner: repositoryInfo.owner,
+    repo: repositoryInfo.repo,
     // eslint-disable-next-line @typescript-eslint/camelcase
     issue_number: pullRequestNumber,
     body: comment
   })
 }
 
-async function findPullRequest(
-  octokit: github.GitHub,
-  owner: string,
-  repo: string,
-  headRepo: string,
-  headBranch: string,
-  headSha: string
-): Promise<rest.PullsListResponseItem | null> {
-  // Finds Pull request for this workflow run
+/**
+ * Notifies PR about cancelling
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - my own run id
+ * @param pullRequestNumber - number of pull request
+ * @param reason reason for canceling
+ */
+async function notifyPR(
+  repositoryInfo: RepositoryInfo,
+  selfRunId: number,
+  pullRequestNumber: number,
+  reason: string
+): Promise<void> {
+  const selfWorkflowRunUrl =
+    `https://github.com/${repositoryInfo.owner}/${repositoryInfo.repo}` +
+    `/actions/runs/${selfRunId}`
+  await addCommentToPullRequest(
+    repositoryInfo,
+    pullRequestNumber,
+    `[The Workflow run](${selfWorkflowRunUrl}) is cancelling this PR. ${reason}`
+  )
+}
+
+/**
+ * Cancels all runs in the specified group of runs.
+ * @param repositoryInfo - information about the repository used
+ * @param sortedRunItems - items sorted in descending order (descending order by created_at)
+ * @param notifyPRCancel - whether to notify the PR when cancelling
+ * @param selfRunId - what is the self run id
+ * @param sourceGroupId - what is the source group id
+ * @param reason - reason for canceling
+ */
+async function cancelAllRunsInTheSourceGroup(
+  repositoryInfo: RepositoryInfo,
+  sortedRunItems: rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[],
+  notifyPRCancel: boolean,
+  selfRunId: number,
+  sourceGroupId: string,
+  reason: string
+): Promise<number[]> {
   core.info(
-    `\nFinding PR request id for: owner: ${owner}, Repo:${repo}, Head:${headRepo}:${headBranch}.\n`
+    `\n###### Cancelling runs for ${sourceGroupId} starting from the most recent  ##########\n` +
+      `\n     Number of runs to cancel: ${sortedRunItems.length}\n`
   )
-  const pullRequests = await octokit.pulls.list({
-    owner,
-    repo,
-    head: `${headRepo}:${headBranch}`
-  })
-  for (const pullRequest of pullRequests.data) {
-    core.info(
-      `\nComparing: ${pullRequest.number} sha: ${pullRequest.head.sha} with expected: ${headSha}.\n`
+  const cancelledRuns: number[] = []
+  for (const runItem of sortedRunItems) {
+    if (notifyPRCancel && runItem.event === 'pull_request') {
+      const pullRequestNumber = await findPullRequestForRunItem(
+        repositoryInfo,
+        runItem
+      )
+      if (pullRequestNumber !== undefined) {
+        core.info(
+          `\nNotifying PR: ${pullRequestNumber} (runItem: ${runItem}) with: ${reason}\n`
+        )
+        await notifyPR(repositoryInfo, selfRunId, pullRequestNumber, reason)
+      }
+    }
+    core.info(`\nCancelling run: ${runItem}.\n`)
+    await cancelRun(repositoryInfo, runItem.id)
+    cancelledRuns.push(runItem.id)
+  }
+  core.info(
+    `\n######  Finished cancelling runs for ${sourceGroupId} ##########\n`
+  )
+  return cancelledRuns
+}
+
+/**
+ * Cancels found runs in a smart way. It takes all the found runs group by the source group, sorts them
+ * descending according to create date in each of the source groups and cancels them in that order -
+ * optionally skipping the first found run in each source group in case of duplicates.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param mapOfWorkflowRunsCandidatesToCancel map of all workflow run candidates
+ * @param cancelMode - cancel mode
+ * @param cancelFutureDuplicates - whether to cancel future duplicates
+ * @param notifyPRCancel - whether to notify PRs with comments
+ * @param selfRunId - self run Id
+ * @param reason - reason for canceling
+ */
+async function cancelTheRunsPerGroup(
+  repositoryInfo: RepositoryInfo,
+  mapOfWorkflowRunsCandidatesToCancel: Map<
+    string,
+    rest.ActionsListWorkflowRunsResponseWorkflowRunsItem[]
+  >,
+  cancelMode: CancelMode,
+  cancelFutureDuplicates: boolean,
+  notifyPRCancel: boolean,
+  selfRunId: number,
+  reason: string
+): Promise<number[]> {
+  const cancelledRuns: number[] = []
+  for (const [
+    sourceGroupId,
+    candidatesArray
+  ] of mapOfWorkflowRunsCandidatesToCancel) {
+    // Sort from most recent date - this way we always kill current one at the end (if we kill it at all)
+    const sortedRunItems = candidatesArray.sort((runItem1, runItem2) =>
+      runItem2.created_at.localeCompare(runItem1.created_at)
     )
-    if (pullRequest.head.sha === headSha) {
-      core.info(`\nFound PR: ${pullRequest.number}\n`)
-      return pullRequest
+    if (sortedRunItems.length > 0) {
+      if (
+        (cancelMode === CancelMode.DUPLICATES && cancelFutureDuplicates) ||
+        cancelMode === CancelMode.ALL_DUPLICATES
+      ) {
+        core.info(
+          `\nSkipping the first run (${sortedRunItems[0].id}) of all the matching ` +
+            `duplicates for '${sourceGroupId}'. This one we are going to leave in peace!\n`
+        )
+        sortedRunItems.shift()
+      }
+      if (sortedRunItems.length === 0) {
+        core.info(`\nNo duplicates to cancel for ${sourceGroupId}!\n`)
+        continue
+      }
+      cancelledRuns.push(
+        ...(await cancelAllRunsInTheSourceGroup(
+          repositoryInfo,
+          sortedRunItems,
+          notifyPRCancel,
+          selfRunId,
+          sourceGroupId,
+          reason
+        ))
+      )
+    } else {
+      core.info(
+        `\n######  There are no runs to cancel for ${sourceGroupId} ##########\n`
+      )
     }
   }
-  core.info(`\nCould not find the PR for this build :(\n`)
-  return null
+  return cancelledRuns
+}
+
+/**
+ * Find and cancels runs based on the criteria chosen.
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - number of own run id
+ * @param sourceWorkflowId - source workflow id that triggered the workflow
+ *        (might be different than self for workflow_run)
+ * @param sourceRunId - source run id that triggered the workflow
+ *        (might be different than self for workflow_run)
+ * @param headRepo - head repository that triggered the workflow (repo from which PR came)
+ * @param headBranch - branch of the PR that triggered the workflow (when it is triggered by PR)
+ * @param sourceEventName - name of the event that triggered the workflow
+ *        (different than self event name for workflow_run)
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether to cancel future duplicates for duplicate cancelling
+ * @param notifyPRCancel - whether to notify in PRs about cancelling
+ * @param notifyPRMessageStart - whether to notify PRs when the action starts
+ * @param jobNameRegexps - array of regular expressions to match hob names in case of named modes
+ * @param skipEventTypes - array of event names that should be skipped
+ * @param reason - reason for cancelling
+ * @return array of canceled workflow run ids
+ */
+async function findAndCancelRuns(
+  repositoryInfo: RepositoryInfo,
+  selfRunId: number,
+  sourceWorkflowId: number | string,
+  sourceRunId: number,
+  headRepo: string,
+  headBranch: string,
+  sourceEventName: string,
+  cancelMode: CancelMode,
+  cancelFutureDuplicates: boolean,
+  notifyPRCancel: boolean,
+  notifyPRMessageStart: string,
+  jobNameRegexps: string[],
+  skipEventTypes: string[],
+  reason: string
+): Promise<number[]> {
+  const statusValues = ['queued', 'in_progress']
+  const mySourceGroup: WorkflowRunSourceGroup = {
+    headBranch,
+    headRepo,
+    eventName: sourceEventName,
+    workflowId: sourceWorkflowId
+  }
+  const workflowRuns = await getWorkflowRunsMatchingCriteria(
+    repositoryInfo,
+    statusValues,
+    cancelMode,
+    sourceWorkflowId,
+    sourceRunId,
+    sourceEventName,
+    mySourceGroup
+  )
+  const mapOfWorkflowRunsCandidatesToCancel = await filterAndMapWorkflowRunsToGroups(
+    repositoryInfo,
+    headRepo,
+    cancelMode,
+    cancelFutureDuplicates,
+    sourceRunId,
+    jobNameRegexps,
+    skipEventTypes,
+    workflowRuns
+  )
+  return await cancelTheRunsPerGroup(
+    repositoryInfo,
+    mapOfWorkflowRunsCandidatesToCancel,
+    cancelMode,
+    cancelFutureDuplicates,
+    notifyPRCancel,
+    selfRunId,
+    reason
+  )
 }
 
+/**
+ * Returns environment variable that is required - throws error if it is not defined.
+ * @param key key for the env variable
+ * @return value of the env variable
+ */
+function getRequiredEnv(key: string): string {
+  const value = process.env[key]
+  if (value === undefined) {
+    const message = `${key} was not defined.`
+    throw new Error(message)
+  }
+  return value
+}
+
+/**
+ * Gets origin of the runId - if this is a workflow run, it returns the information about the source run
+ * @param repositoryInfo - information about the repository used
+ * @param runId - run id of the run to check
+ * @return information about the triggering workflow
+ */
 async function getOrigin(
-  octokit: github.GitHub,
-  runId: number,
-  owner: string,
-  repo: string
-): Promise<
-  [string, string, string, string, string, rest.PullsListResponseItem | null]
-> {
-  const reply = await octokit.actions.getWorkflowRun({
-    owner,
-    repo,
+  repositoryInfo: RepositoryInfo,
+  runId: number
+): Promise<TriggeringWorkflowInfo> {
+  const reply = await repositoryInfo.octokit.actions.getWorkflowRun({
+    owner: repositoryInfo.owner,
+    repo: repositoryInfo.repo,
     // eslint-disable-next-line @typescript-eslint/camelcase
     run_id: runId
   })
@@ -568,32 +993,50 @@ async function getOrigin(
   let pullRequest: rest.PullsListResponseItem | null = null
   if (sourceRun.event === 'pull_request') {
     pullRequest = await findPullRequest(
-      octokit,
-      owner,
-      repo,
+      repositoryInfo,
       sourceRun.head_repository.owner.login,
       sourceRun.head_branch,
       sourceRun.head_sha
     )
   }
 
-  return [
-    reply.data.head_repository.full_name,
-    reply.data.head_branch,
-    reply.data.event,
-    reply.data.head_sha,
-    pullRequest ? pullRequest.merge_commit_sha : '',
-    pullRequest
-  ]
+  return {
+    headRepo: reply.data.head_repository.full_name,
+    headBranch: reply.data.head_branch,
+    headSha: reply.data.head_sha,
+    mergeCommitSha: pullRequest ? pullRequest.merge_commit_sha : null,
+    pullRequest: pullRequest ? pullRequest : null,
+    eventName: reply.data.event
+  }
 }
 
+/**
+ * Performs the actual cancelling.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param selfRunId - number of own run id
+ * @param sourceWorkflowId - source workflow id that triggered the workflow
+ *        (might be different than self for workflow_run)
+ * @param sourceRunId - source run id that triggered the workflow
+ *        (might be different than self for workflow_run)
+ * @param headRepo - head repository that triggered the workflow (repo from which PR came)
+ * @param headBranch - branch of the PR that triggered the workflow (when it is triggered by PR)
+ * @param sourceEventName - name of the event that triggered the workflow
+ *        (different than self event name for workflow_run)
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether to cancel future duplicates for duplicate cancelling
+ * @param notifyPRCancel - whether to notify in PRs about cancelling
+ * @param notifyPRCancelMessage - message to send when cancelling the PR (overrides default message
+ *        generated automatically)
+ * @param notifyPRMessageStart - whether to notify PRs when the action starts
+ * @param jobNameRegexps - array of regular expressions to match hob names in case of named modes
+ * @param skipEventTypes - array of event names that should be skipped
+ */
 async function performCancelJob(
-  octokit: github.GitHub,
+  repositoryInfo: RepositoryInfo,
   selfRunId: number,
   sourceWorkflowId: number | string,
   sourceRunId: number,
-  owner: string,
-  repo: string,
   headRepo: string,
   headBranch: string,
   sourceEventName: string,
@@ -609,7 +1052,7 @@ async function performCancelJob(
     '\n###################################################################################\n'
   )
   core.info(
-    `All parameters: owner: ${owner}, repo: ${repo}, run id: ${sourceRunId}, ` +
+    `All parameters: owner: ${repositoryInfo.owner}, repo: ${repositoryInfo.repo}, run id: ${sourceRunId}, ` +
       `head repo ${headRepo}, headBranch: ${headBranch}, ` +
       `sourceEventName: ${sourceEventName}, cancelMode: ${cancelMode}, jobNames: ${jobNameRegexps}`
   )
@@ -636,9 +1079,14 @@ async function performCancelJob(
     reason = `It has jobs matching ${jobNameRegexps}.`
   } else if (cancelMode === CancelMode.DUPLICATES) {
     core.info(
-      `# Cancel duplicate runs started before ${sourceRunId} for workflow ${sourceWorkflowId}.`
+      `# Cancel duplicate runs for workflow ${sourceWorkflowId} for same triggering branch as own run Id.`
+    )
+    reason = `It is an earlier duplicate of ${sourceWorkflowId} run.`
+  } else if (cancelMode === CancelMode.ALL_DUPLICATES) {
+    core.info(
+      `# Cancel all duplicates runs started for workflow ${sourceWorkflowId}.`
     )
-    reason = `It in earlier duplicate of ${sourceWorkflowId} run.`
+    reason = `It is an earlier duplicate of ${sourceWorkflowId} run.`
   } else {
     throw Error(`Wrong cancel mode ${cancelMode}! This should never happen.`)
   }
@@ -647,12 +1095,10 @@ async function performCancelJob(
   )
 
   return await findAndCancelRuns(
-    octokit,
+    repositoryInfo,
     selfRunId,
     sourceWorkflowId,
     sourceRunId,
-    owner,
-    repo,
     headRepo,
     headBranch,
     sourceEventName,
@@ -666,11 +1112,165 @@ async function performCancelJob(
   )
 }
 
+/**
+ * Retrieves information about source workflow Id. Either from the current event or from the workflow
+ * nme specified. If the file name is not specified, the workflow to act on is either set to self event
+ * or in case of workflow_run event - to the workflow id that triggered the 'workflow_run' event.
+ *
+ * @param repositoryInfo - information about the repository used
+ * @param workflowFileName - optional workflow file name
+ * @param sourceRunId - source run id of the workfow
+ * @param selfRunId - self run id
+ * @return workflow id that is associate with the workflow we are going to act on.
+ */
+async function retrieveWorkflowId(
+  repositoryInfo: RepositoryInfo,
+  workflowFileName: string | null,
+  sourceRunId: number,
+  selfRunId: number
+): Promise<string | number> {
+  let sourceWorkflowId
+  if (workflowFileName) {
+    sourceWorkflowId = workflowFileName
+    core.info(
+      `\nFinding runs for another workflow found by ${workflowFileName} name: ${sourceWorkflowId}\n`
+    )
+  } else {
+    sourceWorkflowId = await getWorkflowId(repositoryInfo, sourceRunId)
+    if (sourceRunId === selfRunId) {
+      core.info(`\nFinding runs for my own workflow ${sourceWorkflowId}\n`)
+    } else {
+      core.info(`\nFinding runs for source workflow ${sourceWorkflowId}\n`)
+    }
+  }
+  return sourceWorkflowId
+}
+/**
+ * Sets output but also prints the output value in the logs.
+ *
+ * @param name name of the output
+ * @param value value of the output
+ */
 function verboseOutput(name: string, value: string): void {
   core.info(`Setting output: ${name}: ${value}`)
   core.setOutput(name, value)
 }
 
+/**
+ * Performs sanity check of the parameters passed. Some of the parameter combinations do not work so they
+ * are verified and in case od unexpected combination found, approrpriate error is raised.
+ *
+ * @param eventName - name of the event to act on
+ * @param sourceRunId - run id of the triggering event
+ * @param selfRunId - our own run id
+ * @param cancelMode - cancel mode used
+ * @param cancelFutureDuplicates - whether future duplicate cancelling is enabled
+ * @param jobNameRegexps - array of regular expression of job names
+ */
+function performSanityChecks(
+  eventName: string,
+  sourceRunId: number,
+  selfRunId: number,
+  cancelMode: CancelMode,
+  cancelFutureDuplicates: boolean,
+  jobNameRegexps: string[]
+): void {
+  if (
+    eventName === 'workflow_run' &&
+    sourceRunId === selfRunId &&
+    cancelMode === CancelMode.DUPLICATES
+  ) {
+    throw Error(
+      `You cannot run "workflow_run" in ${cancelMode} cancelMode without "sourceId" input.` +
+        'It will likely not work as you intended - it will cancel runs which are not duplicates!' +
+        'See the docs for details.'
+    )
+  }
+
+  if (
+    jobNameRegexps.length > 0 &&
+    [
+      CancelMode.DUPLICATES,
+      CancelMode.SELF,
+      CancelMode.ALL_DUPLICATES
+    ].includes(cancelMode)
+  ) {
+    throw Error(`You cannot specify jobNames on ${cancelMode} cancelMode.`)
+  }
+
+  if (cancelMode === CancelMode.ALL_DUPLICATES && !cancelFutureDuplicates) {
+    throw Error(
+      `The  ${cancelMode} cancelMode has to have cancelFutureDuplicates set to true.`
+    )
+  }
+}
+
+/**
+ * Produces basic outputs for the action. This does not include cancelled workflow run id - those are
+ * set after cancelling is done.
+ *
+ * @param triggeringWorkflowInfo
+ */
+function produceBasicOutputs(
+  triggeringWorkflowInfo: TriggeringWorkflowInfo
+): void {
+  verboseOutput('sourceHeadRepo', triggeringWorkflowInfo.headRepo)
+  verboseOutput('sourceHeadBranch', triggeringWorkflowInfo.headBranch)
+  verboseOutput('sourceHeadSha', triggeringWorkflowInfo.headSha)
+  verboseOutput('sourceEvent', triggeringWorkflowInfo.eventName)
+  verboseOutput(
+    'pullRequestNumber',
+    triggeringWorkflowInfo.pullRequest
+      ? triggeringWorkflowInfo.pullRequest.number.toString()
+      : ''
+  )
+  verboseOutput(
+    'mergeCommitSha',
+    triggeringWorkflowInfo.mergeCommitSha
+      ? triggeringWorkflowInfo.mergeCommitSha
+      : ''
+  )
+  verboseOutput(
+    'targetCommitSha',
+    triggeringWorkflowInfo.mergeCommitSha
+      ? triggeringWorkflowInfo.mergeCommitSha
+      : triggeringWorkflowInfo.headSha
+  )
+}
+
+/**
+ * Notifies the PR that the action has started.
+ *
+ * @param repositoryInfo information about the repository
+ * @param triggeringWorkflowInfo information about the triggering workflow
+ * @param sourceRunId run id of the source workflow
+ * @param selfRunId self run id
+ * @param notifyPRMessageStart whether to notify about the start of the action
+ */
+async function notifyActionStart(
+  repositoryInfo: RepositoryInfo,
+  triggeringWorkflowInfo: TriggeringWorkflowInfo,
+  sourceRunId: number,
+  selfRunId: number,
+  notifyPRMessageStart: string
+): Promise<void> {
+  if (notifyPRMessageStart && triggeringWorkflowInfo.pullRequest) {
+    const selfWorkflowRunUrl =
+      `https://github.com/${repositoryInfo.owner}/${repositoryInfo.repo}` +
+      `/actions/runs/${selfRunId}`
+    await repositoryInfo.octokit.issues.createComment({
+      owner: repositoryInfo.owner,
+      repo: repositoryInfo.repo,
+      // eslint-disable-next-line @typescript-eslint/camelcase
+      issue_number: triggeringWorkflowInfo.pullRequest.number,
+      body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
+    })
+  }
+}
+
+/**
+ * Main run method that does everything :)
+ */
 async function run(): Promise<void> {
   const token = core.getInput('token', {required: true})
   const octokit = new github.GitHub(token)
@@ -699,33 +1299,31 @@ async function run(): Promise<void> {
 
   const [owner, repo] = repository.split('/')
 
+  const repositoryInfo: RepositoryInfo = {
+    octokit,
+    owner,
+    repo
+  }
   core.info(
     `\nGetting workflow id for source run id: ${sourceRunId}, owner: ${owner}, repo: ${repo},` +
       ` skipEventTypes: ${skipEventTypes}\n`
   )
-  let sourceWorkflowId
+  const sourceWorkflowId = await retrieveWorkflowId(
+    repositoryInfo,
+    workflowFileName,
+    sourceRunId,
+    selfRunId
+  )
+
+  performSanityChecks(
+    eventName,
+    sourceRunId,
+    selfRunId,
+    cancelMode,
+    cancelFutureDuplicates,
+    jobNameRegexps
+  )
 
-  if (workflowFileName) {
-    sourceWorkflowId = workflowFileName
-    core.info(
-      `\nFinding runs for another workflow found by ${workflowFileName} name: ${sourceWorkflowId}\n`
-    )
-  } else {
-    sourceWorkflowId = await getWorkflowId(octokit, sourceRunId, owner, repo)
-    if (sourceRunId === selfRunId) {
-      core.info(`\nFinding runs for my own workflow ${sourceWorkflowId}\n`)
-    } else {
-      core.info(`\nFinding runs for source workflow ${sourceWorkflowId}\n`)
-    }
-    if (eventName === 'workflow_run' && sourceRunId === selfRunId) {
-      if (cancelMode === CancelMode.DUPLICATES)
-        throw Error(
-          `You cannot run "workflow_run" in ${cancelMode} cancelMode without "sourceId" input.` +
-            'It will likely not work as you intended - it will cancel runs which are not duplicates!' +
-            'See the docs for details.'
-        )
-    }
-  }
   core.info(
     `Repository: ${repository}, Owner: ${owner}, Repo: ${repo}, ` +
       `Event name: ${eventName}, CancelMode: ${cancelMode}, ` +
@@ -733,54 +1331,25 @@ async function run(): Promise<void> {
       `jobNames: ${jobNameRegexps}`
   )
 
-  if (
-    jobNameRegexps.length > 0 &&
-    [CancelMode.DUPLICATES, CancelMode.SELF].includes(cancelMode)
-  ) {
-    throw Error(`You cannot specify jobNames on ${cancelMode} cancelMode.`)
-  }
+  const triggeringWorkflowInfo = await getOrigin(repositoryInfo, sourceRunId)
+  produceBasicOutputs(triggeringWorkflowInfo)
 
-  const [
-    headRepo,
-    headBranch,
-    sourceEventName,
-    headSha,
-    mergeCommitSha,
-    pullRequest
-  ] = await getOrigin(octokit, sourceRunId, owner, repo)
-
-  verboseOutput('sourceHeadRepo', headRepo)
-  verboseOutput('sourceHeadBranch', headBranch)
-  verboseOutput('sourceHeadSha', headSha)
-  verboseOutput('sourceEvent', sourceEventName)
-  verboseOutput(
-    'pullRequestNumber',
-    pullRequest ? pullRequest.number.toString() : ''
+  await notifyActionStart(
+    repositoryInfo,
+    triggeringWorkflowInfo,
+    sourceRunId,
+    selfRunId,
+    notifyPRMessageStart
   )
-  verboseOutput('mergeCommitSha', mergeCommitSha)
-  verboseOutput('targetCommitSha', pullRequest ? mergeCommitSha : headSha)
-
-  const selfWorkflowRunUrl = `https://github.com/${owner}/${repo}/actions/runs/${selfRunId}`
-  if (notifyPRMessageStart && pullRequest) {
-    await octokit.issues.createComment({
-      owner,
-      repo,
-      // eslint-disable-next-line @typescript-eslint/camelcase
-      issue_number: pullRequest.number,
-      body: `${notifyPRMessageStart} [The workflow run](${selfWorkflowRunUrl})`
-    })
-  }
 
   const cancelledRuns = await performCancelJob(
-    octokit,
+    repositoryInfo,
     selfRunId,
     sourceWorkflowId,
     sourceRunId,
-    owner,
-    repo,
-    headRepo,
-    headBranch,
-    sourceEventName,
+    triggeringWorkflowInfo.headRepo,
+    triggeringWorkflowInfo.headBranch,
+    triggeringWorkflowInfo.eventName,
     cancelMode,
     notifyPRCancel,
     notifyPRCancelMessage,