+37
-1
@@ -5,6 +5,42 @@ { | ||
| { | ||
| "date": "Tue, 26 Oct 2021 07:11:58 GMT", | ||
| "date": "Wed, 20 Aug 2025 03:56:20 GMT", | ||
| "version": "1.2.0", | ||
| "tag": "p-graph_v1.2.0", | ||
| "comments": { | ||
| "minor": [ | ||
| { | ||
| "author": "elcraig@microsoft.com", | ||
| "package": "p-graph", | ||
| "commit": "e26e1cdadb50bbc8e8b826455c0eaf62291aa60a", | ||
| "comment": "Update repo-internal Node and TS versions to latest (previous Node compatibility should be maintained), and use type imports/exports where relevant" | ||
| }, | ||
| { | ||
| "author": "elcraig@microsoft.com", | ||
| "package": "p-graph", | ||
| "commit": "e26e1cdadb50bbc8e8b826455c0eaf62291aa60a", | ||
| "comment": "Add a named export for the `pGraph` function" | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| { | ||
| "date": "Tue, 21 Feb 2023 20:16:34 GMT", | ||
| "tag": "p-graph_v1.1.2", | ||
| "version": "1.1.2", | ||
| "comments": { | ||
| "none": [ | ||
| { | ||
| "author": "ecraig12345@gmail.com", | ||
| "package": "p-graph", | ||
| "commit": "4acea4dca5faed6e325b07a778e6c72cb2229208", | ||
| "comment": "Update beachball" | ||
| } | ||
| ] | ||
| } | ||
| }, | ||
| { | ||
| "date": "Tue, 26 Oct 2021 07:12:04 GMT", | ||
| "tag": "p-graph_v1.1.2", | ||
| "version": "1.1.2", | ||
| "comments": { | ||
| "patch": [ | ||
@@ -11,0 +47,0 @@ { |
+11
-2
| # Change Log - p-graph | ||
| This log was last generated on Tue, 26 Oct 2021 07:11:58 GMT and should not be manually modified. | ||
| <!-- This log was last generated on Wed, 20 Aug 2025 03:56:20 GMT and should not be manually modified. --> | ||
| <!-- Start content --> | ||
| ## 1.2.0 | ||
| Wed, 20 Aug 2025 03:56:20 GMT | ||
| ### Minor changes | ||
| - Update repo-internal Node and TS versions to latest (previous Node compatibility should be maintained), and use type imports/exports where relevant (elcraig@microsoft.com) | ||
| - Add a named export for the `pGraph` function (elcraig@microsoft.com) | ||
| ## 1.1.2 | ||
| Tue, 26 Oct 2021 07:11:58 GMT | ||
| Tue, 26 Oct 2021 07:12:04 GMT | ||
@@ -11,0 +20,0 @@ ### Patches |
@@ -1,6 +0,8 @@ | ||
| import { PGraphNodeWithDependencies } from "./types"; | ||
| import type { PGraphNodeWithDependencies } from "./types"; | ||
| /** | ||
| * Returns a JS map that has the "cumulative" priority for each node, which is defined as the priority of the current node plus the maximum cumulative priority amongst all children. | ||
| * This is helpful for identifying which nodes to schedule first in order to get to higher priority nodes more quickly. | ||
| * Returns a JS map that has the "cumulative" priority for each node, which is defined as the | ||
| * priority of the current node plus the maximum cumulative priority amongst all children. | ||
| * This is helpful for identifying which nodes to schedule first in order to get to higher | ||
| * priority nodes more quickly. | ||
| */ | ||
| export declare function getNodeCumulativePriorities(pGraphDependencyMap: Map<string, PGraphNodeWithDependencies>, nodesWithNoDependencies: string[]): Map<string, number>; |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| /** Creates a map of node ids to a set of all the nodes this node depends on. This creates a new copy of the set to enable duplication */ | ||
| exports.getNodeCumulativePriorities = getNodeCumulativePriorities; | ||
| /** | ||
| * Creates a map of node ids to a set of all the nodes this node depends on. | ||
| * This creates a new copy of the set to enable deduplication. | ||
| */ | ||
| function getNewDependsOnMap(pGraphDependencyMap) { | ||
@@ -28,4 +32,6 @@ return new Map([...pGraphDependencyMap.entries()].map(([key, value]) => [key, new Set(value.dependsOn)])); | ||
| /** | ||
| * Returns a JS map that has the "cumulative" priority for each node, which is defined as the priority of the current node plus the maximum cumulative priority amongst all children. | ||
| * This is helpful for identifying which nodes to schedule first in order to get to higher priority nodes more quickly. | ||
| * Returns a JS map that has the "cumulative" priority for each node, which is defined as the | ||
| * priority of the current node plus the maximum cumulative priority amongst all children. | ||
| * This is helpful for identifying which nodes to schedule first in order to get to higher | ||
| * priority nodes more quickly. | ||
| */ | ||
@@ -52,2 +58,1 @@ function getNodeCumulativePriorities(pGraphDependencyMap, nodesWithNoDependencies) { | ||
| } | ||
| exports.getNodeCumulativePriorities = getNodeCumulativePriorities; |
@@ -1,2 +0,2 @@ | ||
| import { PGraphNodeWithCyclicDependency, PGraphNodeWithNoCyclicDependency, PGraphNodeWithDependencies } from "./types"; | ||
| import type { PGraphNodeWithCyclicDependency, PGraphNodeWithNoCyclicDependency, PGraphNodeWithDependencies } from "./types"; | ||
| /** | ||
@@ -3,0 +3,0 @@ * Checks for any cycles in the dependency graph, returning `{ hasCycle: false }` if no cycles were detected. |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.graphHasCycles = graphHasCycles; | ||
| /** | ||
@@ -31,3 +32,2 @@ * Checks for any cycles in the dependency graph, returning `{ hasCycle: false }` if no cycles were detected. | ||
| } | ||
| exports.graphHasCycles = graphHasCycles; | ||
| const searchForCycleDFS = (graph, visitMap, nodeId) => { | ||
@@ -44,3 +44,3 @@ const stack = [{ node: nodeId, traversing: false }]; | ||
| */ | ||
| const listOfCycle = stack.filter(i => i.traversing).map(a => a.node); | ||
| const listOfCycle = stack.filter((i) => i.traversing).map((a) => a.node); | ||
| return listOfCycle.slice(listOfCycle.indexOf(current.node)); | ||
@@ -47,0 +47,0 @@ } |
+3
-3
| import { PGraph } from "./PGraph"; | ||
| import { PGraphNodeMap, DependencyList } from "./types"; | ||
| declare function pGraph(nodeMap: PGraphNodeMap, dependencies: DependencyList): PGraph; | ||
| import type { PGraphNodeMap, DependencyList } from "./types"; | ||
| export declare function pGraph(nodeMap: PGraphNodeMap, dependencies: DependencyList): PGraph; | ||
| export default pGraph; | ||
| export * from "./types"; | ||
| export type * from "./types"; |
+1
-0
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.pGraph = pGraph; | ||
| const PGraph_1 = require("./PGraph"); | ||
@@ -4,0 +5,0 @@ function pGraph(nodeMap, dependencies) { |
+3
-2
@@ -1,6 +0,7 @@ | ||
| import { RunOptions, PGraphNodeMap, DependencyList } from "./types"; | ||
| import type { RunOptions, PGraphNodeMap, DependencyList } from "./types"; | ||
| export declare class PGraph { | ||
| private readonly pGraphDependencyMap; | ||
| /** | ||
| * Tracks all the nodes that are ready to be executed since it is not depending on the results of any non completed tasks. | ||
| * Tracks all the nodes that are ready to be executed since it is not depending on the results | ||
| * of any non completed tasks. | ||
| */ | ||
@@ -7,0 +8,0 @@ private readonly nodesWithNoDependencies; |
+9
-6
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.PGraph = void 0; | ||
| const PriorityQueue_1 = require("./PriorityQueue"); | ||
@@ -26,5 +27,5 @@ const getNodeCumulativePriorities_1 = require("./getNodeCumulativePriorities"); | ||
| if (this.nodesWithNoDependencies.length == 0 && nodeMap.size > 0) { | ||
| throw new Error("We could not find a node in the graph with no dependencies, this likely means there is a cycle including all nodes"); | ||
| throw new Error("We could not find a node in the graph with no dependencies; this likely means there is a cycle including all nodes"); | ||
| } | ||
| const graph = graphHasCycles_1.graphHasCycles(this.pGraphDependencyMap); | ||
| const graph = (0, graphHasCycles_1.graphHasCycles)(this.pGraphDependencyMap); | ||
| if (graph.hasCycle) { | ||
@@ -41,5 +42,5 @@ throw new Error(`A cycle has been detected including the following nodes:\n${graph.cycle.join("\n")}`); | ||
| if (concurrency !== undefined && concurrency < 0) { | ||
| throw new Error(`concurrency must be either undefined or a positive integer, received ${options === null || options === void 0 ? void 0 : options.concurrency}`); | ||
| throw new Error(`concurrency must be either undefined or a positive integer; received ${options === null || options === void 0 ? void 0 : options.concurrency}`); | ||
| } | ||
| const nodeCumulativePriorities = getNodeCumulativePriorities_1.getNodeCumulativePriorities(this.pGraphDependencyMap, this.nodesWithNoDependencies); | ||
| const nodeCumulativePriorities = (0, getNodeCumulativePriorities_1.getNodeCumulativePriorities)(this.pGraphDependencyMap, this.nodesWithNoDependencies); | ||
| const priorityQueue = new PriorityQueue_1.PriorityQueue(); | ||
@@ -80,3 +81,4 @@ this.nodesWithNoDependencies.forEach((itemId) => priorityQueue.insert(itemId, nodeCumulativePriorities.get(itemId))); | ||
| dependentNode.dependsOn.delete(taskToRunId); | ||
| // If the task that just completed was the last remaining dependency for a node, add it to the set of unblocked nodes | ||
| // If the task that just completed was the last remaining dependency for a node, | ||
| // add it to the set of unblocked nodes | ||
| if (dependentNode.dependsOn.size === 0) { | ||
@@ -102,3 +104,4 @@ priorityQueue.insert(dependentId, nodeCumulativePriorities.get(dependentId)); | ||
| } | ||
| while (!priorityQueue.isEmpty() && (concurrency === undefined || currentlyRunningTaskCount < concurrency)) { | ||
| while (!priorityQueue.isEmpty() && | ||
| (concurrency === undefined || currentlyRunningTaskCount < concurrency)) { | ||
| scheduleTask() | ||
@@ -105,0 +108,0 @@ .then(() => trySchedulingTasks()) |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.PriorityQueue = void 0; | ||
| class PriorityQueue { | ||
@@ -4,0 +5,0 @@ constructor() { |
+13
-6
@@ -16,7 +16,8 @@ /** | ||
| */ | ||
| export declare type PGraphNodeMap = Map<string, PGraphNode>; | ||
| export type PGraphNodeMap = Map<string, PGraphNode>; | ||
| /** | ||
| * Describes a dependency between two nodes in the p-graph. For each tuple in the array, the first task must complete before the second one begins | ||
| * Describes a dependency between two nodes in the p-graph. | ||
| * For each tuple in the array, the first task must complete before the second one begins. | ||
| */ | ||
| export declare type DependencyList = [string, string][]; | ||
| export type DependencyList = [string, string][]; | ||
| /** | ||
@@ -26,3 +27,7 @@ * The optional arguments to pass to the run function | ||
| export interface RunOptions { | ||
| /** The maximum amount of promises that can be executing at the same time. When not provided, we do not limit the number of concurrent tasks and run tasks as soon as they are unblocked */ | ||
| /** | ||
| * The maximum amount of promises that can be executing at the same time. | ||
| * When not provided, we do not limit the number of concurrent tasks and run tasks | ||
| * as soon as they are unblocked. | ||
| */ | ||
| concurrency?: number; | ||
@@ -33,7 +38,9 @@ /** Continues the graph even if there's an rejected task */ | ||
| /** | ||
| * An internally used representation of the dependency graph nodes that includes all nodes that this node depends on plus all the nodes that depend on this node. | ||
| * An internally used representation of the dependency graph nodes that includes all nodes that | ||
| * this node depends on plus all the nodes that depend on this node. | ||
| */ | ||
| export interface PGraphNodeWithDependencies extends PGraphNode { | ||
| /** | ||
| * The set of nodes that this node depends on. This node should not be executed until all the nodes in this list have been executed to completion. | ||
| * The set of nodes that this node depends on. This node should not be executed until all the | ||
| * nodes in this list have been executed to completion. | ||
| */ | ||
@@ -40,0 +47,0 @@ dependsOn: Set<string>; |
+23
-8
| { | ||
| "name": "p-graph", | ||
| "version": "1.1.2", | ||
| "version": "1.2.0", | ||
| "license": "MIT", | ||
| "description": "Run a promise graph with concurrency control", | ||
| "main": "lib/index.js", | ||
@@ -11,5 +12,7 @@ "types": "lib/index.d.ts", | ||
| "test": "jest", | ||
| "release": "yarn build && yarn test && beachball publish", | ||
| "release": "beachball publish", | ||
| "checkchange": "beachball check", | ||
| "change": "beachball change" | ||
| "change": "beachball change", | ||
| "format": "prettier --write .", | ||
| "format:check": "prettier --check ." | ||
| }, | ||
@@ -20,8 +23,20 @@ "repository": { | ||
| "devDependencies": { | ||
| "@types/jest": "^25.2.1", | ||
| "beachball": "^1.31.0", | ||
| "jest": "^25.5.3", | ||
| "ts-jest": "^25.4.0", | ||
| "typescript": "^3.8.3" | ||
| "@types/jest": "^30.0.0", | ||
| "beachball": "^2.55.0", | ||
| "jest": "^30.0.0", | ||
| "prettier": "^3.6.2", | ||
| "ts-jest": "^29.0.0", | ||
| "typescript": "~5.9.2" | ||
| }, | ||
| "beachball": { | ||
| "ignorePatterns": [ | ||
| ".*ignore", | ||
| ".github/**/*", | ||
| "jest.config.js", | ||
| "yarn.lock" | ||
| ] | ||
| }, | ||
| "prettier": { | ||
| "printWidth": 100 | ||
| } | ||
| } |
+3
-1
@@ -11,2 +11,4 @@ # p-graph | ||
| `p-graph` does not have a strict Node version requirement, but the syntax used is currently intended to remain compatible with Node 12+. | ||
| ## Usage | ||
@@ -19,3 +21,3 @@ | ||
| ```js | ||
| const { default: pGraph } = require("p-graph"); // ES6 import also works: import pGraph from 'p-graph'; | ||
| const { pGraph } = require("p-graph"); // ES6 import also works: import pGraph from 'p-graph'; | ||
@@ -22,0 +24,0 @@ const nodeMap = new Map([ |
| # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node | ||
| # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions | ||
| name: PR | ||
| on: | ||
| pull_request: | ||
| branches: [master] | ||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| node-version: [12.x] | ||
| steps: | ||
| - uses: actions/checkout@v2 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Use Node.js ${{ matrix.node-version }} | ||
| uses: actions/setup-node@v1 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| - run: yarn | ||
| - run: yarn checkchange | ||
| - run: yarn build | ||
| - run: yarn test |
| # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node | ||
| # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions | ||
| name: Release | ||
| on: | ||
| push: | ||
| branches: [master] | ||
| jobs: | ||
| build: | ||
| runs-on: ubuntu-latest | ||
| strategy: | ||
| matrix: | ||
| node-version: [12.x] | ||
| steps: | ||
| - uses: actions/checkout@v2 | ||
| with: | ||
| fetch-depth: 0 | ||
| token: ${{ secrets.repo_pat }} | ||
| - name: Use Node.js ${{ matrix.node-version }} | ||
| uses: actions/setup-node@v1 | ||
| with: | ||
| node-version: ${{ matrix.node-version }} | ||
| - run: yarn | ||
| - run: yarn build | ||
| - run: yarn test | ||
| - run: | | ||
| git config user.email "kchau@microsoft.com" | ||
| git config user.name "Ken Chau" | ||
| - run: yarn release -y -n $NPM_AUTHTOKEN | ||
| env: | ||
| NPM_AUTHTOKEN: ${{ secrets.npm_authtoken }} |
| { | ||
| // Use IntelliSense to learn about possible Node.js debug attributes. | ||
| // Hover to view descriptions of existing attributes. | ||
| // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 | ||
| "version": "0.2.0", | ||
| "configurations": [ | ||
| { | ||
| "name": "Debug Jest Tests", | ||
| "type": "node", | ||
| "request": "launch", | ||
| "runtimeArgs": ["--inspect-brk", "${workspaceRoot}/node_modules/jest/bin/jest.js", "--runInBand"], | ||
| "console": "integratedTerminal", | ||
| "internalConsoleOptions": "neverOpen", | ||
| "port": 9229 | ||
| } | ||
| ] | ||
| } |
| # Microsoft Open Source Code of Conduct | ||
| This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). | ||
| Resources: | ||
| - [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) | ||
| - [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) | ||
| - Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns |
| module.exports = { | ||
| preset: "ts-jest", | ||
| modulePathIgnorePatterns: ["<rootDir>/lib"], | ||
| testEnvironment: "node", | ||
| }; |
| declare global { | ||
| namespace jest { | ||
| interface Matchers<R> { | ||
| /** | ||
| * Enforces that a particular schedule does not schedule secondTaskName until firstTaskName is complete | ||
| */ | ||
| toHaveScheduleOrdering(firstTaskName: string, secondTaskName: string): R; | ||
| /** | ||
| * Enforces that a particular schedule does not schedule secondTaskName to start before firstTaskName has started | ||
| */ | ||
| toHaveStartedBefore(firstTaskName: string, secondTaskName: string): R; | ||
| /** | ||
| * Enforces that a specific task was executed | ||
| */ | ||
| toHaveScheduledTask(taskName: string): R; | ||
| } | ||
| } | ||
| } | ||
| export {}; |
| "use strict"; | ||
| var __importDefault = (this && this.__importDefault) || function (mod) { | ||
| return (mod && mod.__esModule) ? mod : { "default": mod }; | ||
| }; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| const index_1 = __importDefault(require("../index")); | ||
| class FunctionScheduler { | ||
| constructor() { | ||
| this.currentlyRunningFunctions = []; | ||
| this.tickScheduled = false; | ||
| this.callRecords = []; | ||
| } | ||
| startExecutingFunction(definition) { | ||
| const { name, duration } = definition; | ||
| this.callRecords.push({ name, state: "start" }); | ||
| const promise = new Promise((resolve) => { | ||
| this.currentlyRunningFunctions.push({ name, ticksRemaining: duration, resolve }); | ||
| }); | ||
| this.ensureTickScheduled(); | ||
| return promise; | ||
| } | ||
| ensureTickScheduled() { | ||
| if (!this.tickScheduled) { | ||
| Promise.resolve().then(() => this.tick()); | ||
| this.tickScheduled = true; | ||
| } | ||
| } | ||
| tick() { | ||
| this.tickScheduled = false; | ||
| this.currentlyRunningFunctions.forEach((item) => { | ||
| item.ticksRemaining = item.ticksRemaining - 1; | ||
| }); | ||
| const finishedItems = this.currentlyRunningFunctions.filter((item) => item.ticksRemaining === 0); | ||
| this.currentlyRunningFunctions = this.currentlyRunningFunctions.filter((item) => item.ticksRemaining !== 0); | ||
| if (finishedItems.length > 0) { | ||
| finishedItems.forEach((item) => { | ||
| item.resolve(); | ||
| this.callRecords.push({ name: item.name, state: "end" }); | ||
| }); | ||
| } | ||
| if (this.currentlyRunningFunctions.length > 0) { | ||
| this.ensureTickScheduled(); | ||
| } | ||
| } | ||
| } | ||
| function defineMockNode(definition, functionScheduler) { | ||
| return [definition.name, { run: () => functionScheduler.startExecutingFunction(definition), priority: definition.priority }]; | ||
| } | ||
| expect.extend({ | ||
| toHaveScheduleOrdering(callRecords, firstTaskName, secondTaskName) { | ||
| const firstIndex = callRecords.findIndex((item) => item.name === firstTaskName && item.state === "end"); | ||
| const secondIndex = callRecords.findIndex((item) => item.name === secondTaskName && item.state === "start"); | ||
| const pass = firstIndex !== -1 && secondIndex !== -1 && firstIndex < secondIndex; | ||
| return { | ||
| message: () => `expected ${secondTaskName} to be scheduled after ${firstTaskName} completed`, | ||
| pass, | ||
| }; | ||
| }, | ||
| toHaveStartedBefore(callRecords, firstTaskName, secondTaskName) { | ||
| const firstIndex = callRecords.findIndex((item) => item.name === firstTaskName && item.state === "start"); | ||
| const secondIndex = callRecords.findIndex((item) => item.name === secondTaskName && item.state === "start"); | ||
| const pass = firstIndex !== -1 && secondIndex !== -1 && firstIndex < secondIndex; | ||
| return { | ||
| message: () => `expected ${secondTaskName} to be started after ${firstTaskName} has started`, | ||
| pass, | ||
| }; | ||
| }, | ||
| toHaveScheduledTask(callRecords, taskName) { | ||
| const startIndex = callRecords.findIndex((item) => item.name === taskName && item.state === "end"); | ||
| const endIndex = callRecords.findIndex((item) => item.name === taskName && item.state === "start"); | ||
| const pass = startIndex !== -1 && endIndex !== -1; | ||
| return { | ||
| message: () => `expected to have scheduled task ${taskName}`, | ||
| pass, | ||
| }; | ||
| }, | ||
| }); | ||
| const computeMaxConcurrency = (callRecords) => { | ||
| let currentConcurrency = 0; | ||
| let maxConcurrencySoFar = 0; | ||
| callRecords.forEach((record) => { | ||
| currentConcurrency += record.state === "start" ? 1 : -1; | ||
| maxConcurrencySoFar = Math.max(currentConcurrency, maxConcurrencySoFar); | ||
| }); | ||
| return maxConcurrencySoFar; | ||
| }; | ||
| describe("Public API", () => { | ||
| it("should accept the dependency graph and execute tasks in order", async () => { | ||
| const functionScheduler = new FunctionScheduler(); | ||
| const nodeMap = new Map([ | ||
| defineMockNode({ name: "putOnShirt", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "putOnShorts", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "putOnJacket", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "putOnShoes", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "tieShoes", duration: 1 }, functionScheduler), | ||
| ]); | ||
| const dependencies = [ | ||
| ["putOnShoes", "tieShoes"], | ||
| ["putOnShirt", "putOnJacket"], | ||
| ["putOnShorts", "putOnJacket"], | ||
| ["putOnShorts", "putOnShoes"], | ||
| ]; | ||
| await index_1.default(nodeMap, dependencies).run(); | ||
| const { callRecords } = functionScheduler; | ||
| expect(callRecords).toHaveScheduleOrdering("putOnShoes", "tieShoes"); | ||
| expect(callRecords).toHaveScheduleOrdering("putOnShirt", "putOnJacket"); | ||
| expect(callRecords).toHaveScheduleOrdering("putOnShorts", "putOnJacket"); | ||
| expect(callRecords).toHaveScheduleOrdering("putOnShorts", "putOnShoes"); | ||
| }); | ||
| it("throws an exception when the dependency graph has a cycle starting from the root", async () => { | ||
| const nodeMap = new Map([ | ||
| ["A", { run: () => Promise.resolve() }], | ||
| ["B", { run: () => Promise.resolve() }], | ||
| ["C", { run: () => Promise.resolve() }], | ||
| ]); | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["B", "C"], | ||
| ["C", "A"], | ||
| ]; | ||
| expect(() => index_1.default(nodeMap, dependencies)).toThrow(); | ||
| }); | ||
| it("throws an exception with detailed message when the dependency graph has a cycle", async () => { | ||
| // This is almost the same as the last test, except the root node is not a part of the cycle | ||
| const nodeMap = new Map([ | ||
| ["A", { run: () => Promise.resolve() }], | ||
| ["B", { run: () => Promise.resolve() }], | ||
| ["C", { run: () => Promise.resolve() }], | ||
| ["D", { run: () => Promise.resolve() }], | ||
| ]); | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["B", "C"], | ||
| ["C", "D"], | ||
| ["D", "B"], | ||
| ]; | ||
| const expectedErrorMessage = `A cycle has been detected including the following nodes: | ||
| B | ||
| C | ||
| D`; | ||
| expect(() => index_1.default(nodeMap, dependencies)).toThrow(expectedErrorMessage); | ||
| }); | ||
| it("throws an exception in the first instance of a cycle that has been detected when there are overlapped cycles", async () => { | ||
| // This is almost the same as the last test, except the root node is not a part of the cycle | ||
| const nodeMap = new Map([ | ||
| ["A", { run: () => Promise.resolve() }], | ||
| ["B", { run: () => Promise.resolve() }], | ||
| ["C", { run: () => Promise.resolve() }], | ||
| ["D", { run: () => Promise.resolve() }], | ||
| ["E", { run: () => Promise.resolve() }], | ||
| ["F", { run: () => Promise.resolve() }], | ||
| ]); | ||
| // B -> C -> E -> F -> D is the first cycle detected | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["B", "C"], | ||
| ["C", "D"], | ||
| ["D", "B"], | ||
| ["C", "E"], | ||
| ["E", "F"], | ||
| ["F", "D"], | ||
| ]; | ||
| const expectedErrorMessage = `A cycle has been detected including the following nodes: | ||
| B | ||
| C | ||
| E | ||
| F | ||
| D`; | ||
| expect(() => index_1.default(nodeMap, dependencies)).toThrow(expectedErrorMessage); | ||
| }); | ||
| it("resolves an empty dependnecy graph", async () => { | ||
| const nodeMap = new Map(); | ||
| const dependencies = []; | ||
| expect(index_1.default(nodeMap, dependencies).run()).resolves.toBeUndefined(); | ||
| }); | ||
| it("throws an exception when run is invoked and a task rejects its promise", async () => { | ||
| const nodeMap = new Map([ | ||
| ["A", { run: () => Promise.resolve() }], | ||
| ["B", { run: () => Promise.resolve() }], | ||
| ["C", { run: () => Promise.reject("C rejected") }], | ||
| ]); | ||
| // A | ||
| // B C | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["A", "C"], | ||
| ]; | ||
| await expect(index_1.default(nodeMap, dependencies).run()).rejects.toContain("C rejected"); | ||
| }); | ||
| it("throws an exception, but continues to run the entire graph", async () => { | ||
| const runFns = { | ||
| A: jest.fn().mockReturnValue(Promise.resolve()), | ||
| B: jest.fn().mockReturnValue(Promise.resolve()), | ||
| D: jest.fn().mockReturnValue(Promise.resolve()), | ||
| E: jest.fn().mockReturnValue(Promise.resolve()), | ||
| F: jest.fn().mockReturnValue(Promise.resolve()) | ||
| }; | ||
| const nodeMap = new Map([ | ||
| ["A", { run: runFns.A }], | ||
| ["B", { run: runFns.B }], | ||
| ["C", { run: () => Promise.reject("C rejected") }], | ||
| ["D", { run: runFns.D }], | ||
| ["E", { run: runFns.E }], | ||
| ["F", { run: runFns.F }], | ||
| ]); | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["A", "C"], | ||
| ["A", "D"], | ||
| ["C", "D"], | ||
| ["A", "E"], | ||
| ["E", "F"], | ||
| ]; | ||
| await expect(index_1.default(nodeMap, dependencies).run({ concurrency: 1, continue: true })).rejects.toContain("C rejected"); | ||
| expect(runFns.E).toHaveBeenCalled(); | ||
| expect(runFns.F).toHaveBeenCalled(); | ||
| expect(runFns.D).not.toHaveBeenCalled(); | ||
| }); | ||
| it("throws when one of the dependencies references a node not in the node map", async () => { | ||
| const nodeMap = new Map([ | ||
| ["A", { run: () => Promise.resolve() }], | ||
| ["B", { run: () => Promise.resolve() }], | ||
| ]); | ||
| // A | ||
| // B C | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["A", "C"], | ||
| ]; | ||
| expect(() => index_1.default(nodeMap, dependencies)).toThrow(); | ||
| }); | ||
| it("should run all dependencies for disconnected graphs", async () => { | ||
| const functionScheduler = new FunctionScheduler(); | ||
| const nodeMap = new Map([ | ||
| defineMockNode({ name: "A", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "B", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "C", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "D", duration: 1 }, functionScheduler), | ||
| ]); | ||
| // A D | ||
| // B C | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["A", "C"], | ||
| ]; | ||
| await index_1.default(nodeMap, dependencies).run(); | ||
| const { callRecords } = functionScheduler; | ||
| expect(callRecords).toHaveScheduledTask("A"); | ||
| expect(callRecords).toHaveScheduledTask("B"); | ||
| expect(callRecords).toHaveScheduledTask("C"); | ||
| expect(callRecords).toHaveScheduledTask("D"); | ||
| }); | ||
| it("should be able to run more than one task at a time", async () => { | ||
| const functionScheduler = new FunctionScheduler(); | ||
| const nodeMap = new Map([ | ||
| defineMockNode({ name: "A", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "B", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "C", duration: 1 }, functionScheduler), | ||
| ]); | ||
| // A | ||
| // B C | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["A", "C"], | ||
| ]; | ||
| await index_1.default(nodeMap, dependencies).run(); | ||
| // B and C should run concurrently | ||
| expect(computeMaxConcurrency(functionScheduler.callRecords)).toEqual(2); | ||
| }); | ||
| it("should not exceed maximum concurrency", async () => { | ||
| const functionScheduler = new FunctionScheduler(); | ||
| const funcs = new Map([ | ||
| defineMockNode({ name: "A", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "B", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "C", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "D", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "E", duration: 1 }, functionScheduler), | ||
| ]); | ||
| // A | ||
| // B C D E | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["A", "C"], | ||
| ["A", "D"], | ||
| ["A", "E"], | ||
| ]; | ||
| await index_1.default(funcs, dependencies).run({ concurrency: 3 }); | ||
| expect(computeMaxConcurrency(functionScheduler.callRecords)).toBeLessThanOrEqual(3); | ||
| }); | ||
| it("correctly schedules tasks that have more than one dependency", async () => { | ||
| const functionScheduler = new FunctionScheduler(); | ||
| const funcs = new Map([ | ||
| defineMockNode({ name: "A", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "B", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "C", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "D", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "E", duration: 1 }, functionScheduler), | ||
| ]); | ||
| // All nodes depend on A, D depends on C and B as well | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["A", "C"], | ||
| ["A", "D"], | ||
| ["A", "E"], | ||
| ["C", "D"], | ||
| ["B", "D"], | ||
| ]; | ||
| await index_1.default(funcs, dependencies).run(); | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("A", "B"); | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("A", "C"); | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("A", "D"); | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("A", "E"); | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("B", "D"); | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("C", "D"); | ||
| }); | ||
| it("should schedule high priority tasks and dependencies before lower priority tasks", async () => { | ||
| const functionScheduler = new FunctionScheduler(); | ||
| const funcs = new Map([ | ||
| defineMockNode({ name: "A", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "B", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "C", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "D", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "E", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "F", duration: 1, priority: 16 }, functionScheduler), | ||
| ]); | ||
| // A | ||
| // B C D | ||
| // |E F| | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["A", "C"], | ||
| ["A", "D"], | ||
| ["C", "E"], | ||
| ["C", "F"], | ||
| ]; | ||
| // Set concurrency to 1 to make it easier to validate execution order | ||
| await index_1.default(funcs, dependencies).run({ concurrency: 1 }); | ||
| // A -> C -> F is the critical path, it should be built first | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("C", "B"); | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("C", "D"); | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("F", "E"); | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("F", "B"); | ||
| expect(functionScheduler.callRecords).toHaveScheduleOrdering("F", "D"); | ||
| }); | ||
| it("should schedule high priority tasks and dependencies before lower priority tasks when maxConcurrency is greater than 1", async () => { | ||
| const functionScheduler = new FunctionScheduler(); | ||
| const funcs = new Map([ | ||
| defineMockNode({ name: "A", duration: 1 }, functionScheduler), | ||
| defineMockNode({ name: "B", duration: 16, priority: 16 }, functionScheduler), | ||
| defineMockNode({ name: "C", duration: 4, priority: 4 }, functionScheduler), | ||
| defineMockNode({ name: "D", duration: 4, priority: 4 }, functionScheduler), | ||
| defineMockNode({ name: "E", duration: 12, priority: 12 }, functionScheduler), | ||
| defineMockNode({ name: "F", duration: 16, priority: 16 }, functionScheduler), | ||
| ]); | ||
| // A | ||
| // B C D | ||
| // |E F| | ||
| const dependencies = [ | ||
| ["A", "B"], | ||
| ["A", "C"], | ||
| ["A", "D"], | ||
| ["C", "E"], | ||
| ["C", "F"], | ||
| ]; | ||
| // Set concurrency to 1 to make it easier to validate execution order | ||
| await index_1.default(funcs, dependencies).run({ concurrency: 2 }); | ||
| // A -> C -> F is the critical path, it should be built first | ||
| expect(computeMaxConcurrency(functionScheduler.callRecords)).toBeLessThanOrEqual(2); | ||
| expect(functionScheduler.callRecords).toHaveStartedBefore("C", "B"); | ||
| expect(functionScheduler.callRecords).toHaveStartedBefore("C", "D"); | ||
| expect(functionScheduler.callRecords).toHaveStartedBefore("B", "D"); | ||
| expect(functionScheduler.callRecords).toHaveStartedBefore("F", "E"); | ||
| }); | ||
| }); |
| module.exports = { | ||
| printWidth: 140, | ||
| tabWidth: 2 | ||
| }; |
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
66
3.13%39868
-31.62%6
20%18
-30.77%774
-31.87%1
Infinity%