CacheControl / json-rules-engine

A rules engine expressed in JSON
ISC License
2.53k stars 453 forks source link

Rule chaining not working with Promise.all #303

Open collegewap opened 1 year ago

collegewap commented 1 year ago

If I run the engine sequentially, I am getting the desired result. If I run it in parallel, I am getting results only for the first run.

student-fav-subject.js

const { Engine } = require("json-rules-engine");

const apiClient = require("./student-api-client");

let engine = new Engine();

async function start() {
  let mathematicsSubjectRule = {
    conditions: {
      all: [
        {
          fact: "student-information",
          operator: "contains",
          value: "Mathematics",
          path: "$.subjects",
        },
      ],
    },
    event: { type: "studies-mathematics" },
    priority: 10,
    onSuccess: async function (event, almanac) {
      almanac.addRuntimeFact("studiesMathematics", true);
    },
    onFailure: function (event, almanac) {
      almanac.addRuntimeFact("studiesMathematics", false);
    },
  };
  engine.addRule(mathematicsSubjectRule);

  let physicsFavoriteRule = {
    conditions: {
      all: [
        {
          fact: "studiesMathematics",
          operator: "equal",
          value: true,
        },
        {
          fact: "student-information",
          operator: "equal",
          value: "Physics",
          path: "$.favoriteSubject",
        },
      ],
    },
    event: {
      type: "physics-favorite-subject",
      params: {
        message: " has Physics as their favorite subject",
      },
    },
  };

  engine.addRule(physicsFavoriteRule);

  engine.addFact("student-information", function (params, almanac) {
    console.log("loading student information...");
    return almanac.factValue("studentName").then((studentName) => {
      return apiClient.getStudentInformation(studentName);
    });
  });

  engine.on("success", (event, almanac, ruleResult) => {
    almanac.factValue("studentName").then((studentName) => {
      switch (event.type) {
        case "physics-favorite-subject":
          console.log(
            studentName,
            "HAS".green,
            "Physics as their favorite subject"
          );
      }
    });
  });

  engine.on("failure", (event, almanac, ruleResult) => {
    almanac.factValue("studentName").then((studentName) => {
      switch (event.type) {
        case "physics-favorite-subject":
          const studiesMathResult = ruleResult.conditions.all.find(
            (condition) => condition.fact === "studiesMathematics"
          );
          if (studiesMathResult) {
            if (studiesMathResult.result) {
              console.log(
                studentName,
                "DOES NOT".red,
                "have Physics as favorite subject"
              );
            } else {
              console.log(
                studentName,
                "CANNOT NOT".red,
                "have Physics as favorite subject as they do not study Mathematics"
              );
            }
          }
      }
    });
  });

  Promise.all([
    engine.run({ studentName: "samuel" }),
    engine.run({ studentName: "joseph" }),
    engine.run({ studentName: "anthony" }),
  ]);

  // await engine.run({ studentName: "samuel" });
  // await engine.run({ studentName: "joseph" });
  // await engine.run({ studentName: "anthony" });
}

start();

student-api-client.js

"use strict";

require("colors");
const fs = require("fs");

let studentData = require("./student-data.json");

/**
 * mock api client for retrieving student information
 */
module.exports = {
  getStudentInformation: (studentName) => {
    const message = 'loading student information for "' + studentName + '"';
    console.log(message.dim);
    return new Promise((resolve, reject) => {
      setImmediate(() => {
        resolve(studentData[studentName]);
      });
    });
  },
};

student-data.json

{
  "john": {
    "favoriteSubject": "Mathematics",
    "subjects": ["Physics", "Mathematics"]
  },
  "anthony": {
    "favoriteSubject": "Physics",
    "subjects": ["Physics", "Mathematics"]
  },
  "george": {
    "favoriteSubject": "History",
    "subjects": ["History", "Mathematics"]
  },
  "samuel": {
    "favoriteSubject": "Physics",
    "subjects": ["Physics", "History", "Geography", "Chemistry"]
  },
  "joseph": {
    "favoriteSubject": "Economics",
    "subjects": ["Economics", "Mathematics", "History"]
  }
}

Output

Running sequentially

loading student information...
loading student information for "samuel"
samuel CANNOT NOT have Physics as favorite subject as they do not study Mathematics
loading student information...
loading student information for "joseph"
joseph DOES NOT have Physics as favorite subject
loading student information...
loading student information for "anthony"
anthony HAS Physics as their favorite subject

Running parallelly (Promise.all)

loading student information...
loading student information...
loading student information...
loading student information for "samuel"
loading student information for "joseph"
loading student information for "anthony"
samuel CANNOT NOT have Physics as favorite subject as they do not study Mathematics
collegewap commented 1 year ago

The issue can be reproduced with the example as well.

"use strict";
/*
 * This is an advanced example demonstrating rules that passed based off the
 * results of other rules by adding runtime facts.  It also demonstrates
 * accessing the runtime facts after engine execution.
 *
 * Usage:
 *   node ./examples/07-rule-chaining.js
 *
 * For detailed output:
 *   DEBUG=json-rules-engine node ./examples/07-rule-chaining.js
 */

require("colors");
const { Engine } = require("json-rules-engine");
const { getAccountInformation } = require("./account-api-client");

async function start() {
  /**
   * Setup a new engine
   */
  const engine = new Engine();

  /**
   * Rule for identifying people who may like screwdrivers
   */
  const drinkRule = {
    conditions: {
      all: [
        {
          fact: "drinksOrangeJuice",
          operator: "equal",
          value: true,
        },
        {
          fact: "enjoysVodka",
          operator: "equal",
          value: true,
        },
      ],
    },
    event: { type: "drinks-screwdrivers" },
    priority: 10, // IMPORTANT!  Set a higher priority for the drinkRule, so it runs first
    onSuccess: async function (event, almanac) {
      almanac.addRuntimeFact("screwdriverAficionado", true);

      // asychronous operations can be performed within callbacks
      // engine execution will not proceed until the returned promises is resolved
      const accountId = await almanac.factValue("accountId");
      const accountInfo = await getAccountInformation(accountId);
      almanac.addRuntimeFact("accountInfo", accountInfo);
    },
    onFailure: function (event, almanac) {
      almanac.addRuntimeFact("screwdriverAficionado", false);
    },
  };
  engine.addRule(drinkRule);

  /**
   * Rule for identifying people who should be invited to a screwdriver social
   * - Only invite people who enjoy screw drivers
   * - Only invite people who are sociable
   */
  const inviteRule = {
    conditions: {
      all: [
        {
          fact: "screwdriverAficionado", // this fact value is set when the drinkRule is evaluated
          operator: "equal",
          value: true,
        },
        {
          fact: "isSociable",
          operator: "equal",
          value: true,
        },
        {
          fact: "accountInfo",
          path: "$.company",
          operator: "equal",
          value: "microsoft",
        },
      ],
    },
    event: { type: "invite-to-screwdriver-social" },
    priority: 5, // Set a lower priority for the drinkRule, so it runs later (default: 1)
  };
  engine.addRule(inviteRule);

  /**
   * Register listeners with the engine for rule success and failure
   */
  engine
    .on("success", async (event, almanac) => {
      const accountInfo = await almanac.factValue("accountInfo");
      const accountId = await almanac.factValue("accountId");
      console.log(
        `${accountId}(${accountInfo.company}) ` +
          "DID".green +
          ` meet conditions for the ${event.type.underline} rule.`
      );
    })
    .on("failure", async (event, almanac) => {
      const accountId = await almanac.factValue("accountId");
      console.log(
        `${accountId} did ` +
          "NOT".red +
          ` meet conditions for the ${event.type.underline} rule.`
      );
    });

  // // define fact(s) known at runtime
  // let facts = {
  //   accountId: "washington",
  //   drinksOrangeJuice: true,
  //   enjoysVodka: true,
  //   isSociable: true,
  //   accountInfo: {},
  // };

  // // first run, using washington's facts
  // let results = await engine.run(facts);

  // // isScrewdriverAficionado was a fact set by engine.run()
  // let isScrewdriverAficionado = results.almanac.factValue(
  //   "screwdriverAficionado"
  // );
  // console.log(
  //   `${facts.accountId} ${
  //     isScrewdriverAficionado ? "IS".green : "IS NOT".red
  //   } a screwdriver aficionado`
  // );

  // facts = {
  //   accountId: "jefferson",
  //   drinksOrangeJuice: true,
  //   enjoysVodka: false,
  //   isSociable: true,
  //   accountInfo: {},
  // };
  // results = await engine.run(facts); // second run, using jefferson's facts; facts & evaluation are independent of the first run

  // isScrewdriverAficionado = await results.almanac.factValue(
  //   "screwdriverAficionado"
  // );
  // console.log(
  //   `${facts.accountId} ${
  //     isScrewdriverAficionado ? "IS".green : "IS NOT".red
  //   } a screwdriver aficionado`
  // );

  await engine.run({
    accountId: "jefferson",
    drinksOrangeJuice: true,
    enjoysVodka: false,
    isSociable: true,
    accountInfo: {},
  });
  await engine.run({
    accountId: "washington",
    drinksOrangeJuice: true,
    enjoysVodka: true,
    isSociable: true,
    accountInfo: {},
  });

  // Promise.all([
  //   engine.run({
  //     accountId: "jefferson",
  //     drinksOrangeJuice: true,
  //     enjoysVodka: false,
  //     isSociable: true,
  //     accountInfo: {},
  //   }),
  //   engine.run({
  //     accountId: "washington",
  //     drinksOrangeJuice: true,
  //     enjoysVodka: true,
  //     isSociable: true,
  //     accountInfo: {},
  //   }),
  // ]);
}

start();

/*
 * OUTPUT:
 *
 * loading account information for "washington"
 * washington(microsoft) DID meet conditions for the drinks-screwdrivers rule.
 * washington(microsoft) DID meet conditions for the invite-to-screwdriver-social rule.
 * washington IS a screwdriver aficionado
 * jefferson did NOT meet conditions for the drinks-screwdrivers rule.
 * jefferson did NOT meet conditions for the invite-to-screwdriver-social rule.
 * jefferson IS NOT a screwdriver aficionado
 */