Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Socket
Sign inDemoInstall

clues

Package Overview
Dependencies
Maintainers
2
Versions
158
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

clues

Lightweight logic tree solver using promises.


Version published
Weekly downloads
551
decreased by-11.27%
Maintainers
2
Weekly downloads
 
Created
Source

Complete rewrite in version 3.0

Promises provide a very effective mechanism to construct complex interactions between asynchronous functions. Most promise libraries focus on the promise object itself, and leave the actual structuring of complex logic up to the user.

clues.js simplifies structuring of long complex chains of logic by recursively and automatically solving dependencies /for any given logical operation. Clues is essentially a generalized 'getter' function on an object that returns a promise on the resolved value.

The first argument is the object being queried and the second argument is either:

  • name of the property to be resolved
  • or a custom function whose argument names will be resolved to values before function is executed

If a property is a value or a promise on a value, the value will be returned as a promise on that value. If the property is a function, it will be replaced with a promise on the resulting value from the function evaluation. If the function has arguments, the arguments will be resolved (within the current object scope) before the function is evaluated. Any errors along the way will permutate to all children. The resolution process for a single property can easily become a traversal of a long interlinked chain of dependencies, with automatic memoization as functions become replaced with promises.

As the supplied object is modified by replacing functions with promises, the general use pattern is to use a prototype for logic functions and instantiate a new object for each unique "fact sspace".

Basic structure

Here is a simple example:

//First we define a prototype of the logic, which can be a blend of constants and functions:
var Logic = {
  a : 1,
  b : function(a) {
    return a+1;
  },
}

// Create a facts object from the prototype
var facts = Object.create(Logic);

// Solve for b and print the value (or the error)
clues(facts,'b').then(console.log,console.log);

// Solve for custom function that depends on a and b and prints out the values
clues(facts,function(a,b) {
  console.log(a,b);  // prints  1 2
});

In this example the value a was given, but the property b required a to be resolved before returning a+1; The function is synchronous and the value is known. The example would be very similar if the function was asynchronous.

If we manually change b to a new function that returns a promise on returning a+1 after slight delay, we get the same outcome:

facts.b = function(a) { 
  return Promise.delay(1000)
    .then(function() {
      return a+1;
    })
}

clues(facts,'b').then(console.log)
Minification

Since clues uses argument names to find facts, and javascript minifiction often changes function names, minification does not work out-of-the box. However, logic functions can be defined in array form, where the function itself is placed in the last element, with the other elements representing argument names required for the function, allowing for minification of code that uses clues.js.

In the following example, the local variable a stands for the input1 fact and b is the input2 fact

api.test = ['input1','input2',function(a,b) {
  ... function body ...
}]
Nesting

Logic trees can contain clues objects that provide a separate fact/logic space for any sub-components. Trees can be traversed using dot notation, either when solving by string or using the minification definition above. See recursive-test.js in the test directory for examples.

Example:


var Cabinet = {
  drawer1 : {
    items : [
      'A','B','C
    ]
  }
  thirdItem = ['drawer1.items.2',function(d) { return d;}]
}

clues(Cabinet)



A long chain of dependencies 

If we subsequently ask for the same 'b' from the same facts we get the answer immediately as the





The standard way to use clues is to construct the logic functions 




The first secret ingredient of clues.js is the inspection of function signatures. Argument names of supplied functions are parsed and matched to corresponding facts and logic functions (by name).   The second secret ingredient is the use of promises for all resolutions, callbacks and returns (using the engine/adapter of your choice).

Each argument of a logic function needs to have either a fact with the same name or a logic function with that name (if fact hasn't been determined yet).   If a `fact` with the same name as a function argument already exists (either previously resolved, or supplied directly), it is simply passed on as an argument value to the function.  If a fact is unresolved (i.e. undefined), a logic function with same name is executed to resolve the fact value, before passing it on to the original function for execution.  If neither a fact nor logic can be found with a given reference, a final check is made to see if this reference can be found in the local variables supplied or the clues object itself, before returning an error.

Each logic function will only execute when its input arguments values are known (as a `fact`).  Each unknown input value will start off a `logic` function (by same name).  When a named logic function is executed, a corresponding fact is created as a promise on the results.   When the function has been resolved, the fact table is updated and the promise is fulfilled, allowing any derived functions to proceed.

Each logic function will execute only once (if at all) in an clues object. Multiple requests for the same logic will simply attach to the initial promise, and once the output is known, any further requests will simply return the resolved `fact` (or an error, respectively).

A clues object can be long-running, building up/memoizing all the facts as required or a quick temporary scaffold that is discarded once a particular answer has been recursively solved from the given inputs.

##### Minification

Since clues uses argument names to find facts, and javascript minifiction often changes function names, minification does not work out-of-the box. However, logic functions can be defined in array form, where the function itself is placed in the last element, with the other elements representing argument names required for the function, allowing for minification of code that uses clues.js.

In the following example, the local variable `a` stands for the `input1` fact and `b` is the `input2` fact

api.test = ['input1','input2',function(a,b) { ... function body ... }]


##### Nesting
Logic trees can contain clues objects that provide a separate fact/logic space for any sub-components.  This allows for example person1 and person2 to have identical logic trees without sharing the same facts.   Trees can be traversed using dot notation, either when solving by string or using the minification definition above.   See `recursive-test.js` in the test directory for examples.  (todo: more docs).  Nesting can be turned off by defining the `ignoreDots` option when the clues object is created.

## API Reference

### `clues([logic],[facts],[options])`
Creates a new clues object based on a particular logic (set of functions and values by reference) and facts (associative arrays of known values).  Logic and fact objects can be defined/modified later as they are public variables of the clues object.

The logic object is used as read-only (i.e. any results will **only** alter fact object, not the logic object), allowing the same logic definition to be reused for multiple clues objects (each with a separate fact space).  Clues object can furthermore be chained by requiring any logic function of one clues object to use .exec() function of another.

If no logic is provided to clues in a browser environment, it will use the global object (Window).

An optional `fallback` function can be defined in the options object.  This function will be called whenever a reference can not be found in current fact/logic space and must return a promise.

To turn off nesting (see below) define the `ignoreDots` option as `true`.

### `clues.logic = {}`
Key/value dictionary of logic functions and/or values.   Functions in the logic object must only contain arguments names that correspond to either facts or as other logic functions/values.    Each logic function must either return a value or a promise.

Any logic property that is not a function will be assumed to be a default value for the same fact. This is only recommended for global constants, as the logic elements can be asynchronously used by different clues objects under asymmetric information (facts).

### `clues.facts = {}`
Key value dictionary of facts.  Facts can be user supplied, determined by logic functions or both.   Any fact that exists will prevent execution of logic by same name.  

### `clues.solve(function(arg1,arg2...) { ... } ,[local_vars])`
##### Supplied Function
This schedules an execution of the supplied function.  The argument names of the function will be parsed and matched to facts and logic (and `local` and `self`) within the instance object by argument name.  Any arguments that point to neither a fact nor logic (nor locals or properties of the clues object itself) will result in an error (unless prefixed with an underscore, making it optional).  The supplied function is essentially a callback function that is executed when the inputs are known.  This function returns a promise on its results.

Properties of the clues object (except functions) can be injected into the function arguments when referenced by name. Those include: `self`, `facts`, `logic`.  Additionaly the `local` keyword can be injected to get any local variables submitted, or the local variable can be injected by name (if defined in the local object).  Function of the clues object can be accessed through top level `this` or the injected `self` which retains the right reference inside nested function.  For example, `self.solve`, `self.solver` and `self.fork` provide functionality linked to the respective clues object that called the function.  Finally the Promise adaptor can be referenced through `self.Promise`.

These properties are also available through the `this` context supplied to the logic function.

##### Local Variables
The second argument (optional) allows local variables that will be used, and have priority, against facts and logic.  The is to provide the flexibility to have functions respond to request specific variables, such as a response stream, or to override any previously determined facts.  Please note however, that locals should really be used at end-points in logic, to ensure that locals do not contaminate derived facts.

##### Optional facts
By default all arguments have to be resolved for a function to be evaluated.  Any fact (i.e. argument name) can however be made optional by prefixing it with an underscore.  Any unresolved fact that is optional (either missing or an error) will show as `undefined` inside the function.  The following function will fail if fact `A` can not be resolved, but return `[fact_A,null]` if fact A exists but fact B can not be resolved (or returns an error):

```js
clues.solve(A,_B) {
  return [A,_B]
}

Please keep in mind that any variable that is referred to as optional will look for the un-prefixed name in the fact/logic tables. A fact can therefore be optional in one function (name prefixed with underscore) and required in another (no prefix).

Return = promise

The solve function always returns a promise. As the main operations take place in the user supplied function, the subsequent promise might be of less interest, except for error handling.

Errors

Errors will include a ref property showing which logic function (by name) is raising the error. If the thrown error is not an object (i.e. a string), the resulting error will be a (generic) object with message showing the thrown message and ref the logic function. If the erroring function was called by a named logic function, the name of that function will show up in the caller property of the response. This error handling will not force errors into Error Objects, which can be useful to distinguish between javascript errors (which are Error Object with .stack) and customer 'string' error messages (which do not have .stack).

Example: padding string errors to the client and masking javascript error messages

  .then(null,function(e) {
    if (e.stack) res.end(500,'Internal Error');
    else res.end(e.message);
  });

clues.solve("name",[local_vars])

The solve function can also be called with a string name as first parameter and an optional locals object as the second parameter. This is essentially the same as calling clues solve with a function with only one variable (i.e. name) and is ideal if you need to only work with one fact variable at a time. As before if the name if prefixed with an underscore it is considered optional and returns null if unresolved.

clues.solver("name",[local_vars])

Creates a wrapper around the solve method, making it easier to define new solution routes inside .then().

For example, if we first solve for A but want to solve for B only after and if A fails in a logic function:

logic.data = function() {
  return this.solve('A')
    .then(null,function() {
      return self.solve('B')
    });
}

or use the solver:

logic.data = function(solver) {
  return this.solve('A')
    .then(null,solver('B'))
}

clues.fork([update])

This function creates a new clues object that uses the same logic as the parent, but creates a new fact space inherited from the parent. Any changes to the new fact space will not affect the parent.

Example: If we start with the following clues object and fork it:

var facts = {a: 1, b: 2},
    logic = {c : function(a,b) { return a + 1;},
    c = clues(logic,facts),
    f = c.fork({a: 10});

f.solve('c')
  .then(...)

The fork (f) will end up with the fact space: {a: 10, b: 2, c: 11} leaving the parent facts unchanged.

clues.prototype.Promise

Clues.js is designed to work with any Promise library that supports the Promises/A+ specification through adaptors. By default we use bluebird as it's super-fast, supports cancellation and works well both in the browser and node.js environments. By overwriting the prototype object (or instance property) other libraries can be easily applied. The Promise prototype is available as an injected parameter in any logic function, eliminating the need to specifically require the underlying promise library in the logic functions.

Cancellability

Each solve-promise is cancellable, but the ability to cancel only reaches those logic functions that return cancellable promises. Any facts already established before a cancel is issued, will not be affected. Here is a pseudo example of how such cancellation could be incorporated:

logic.transactions = function(userid,Promise) {
  var connection = db.connect({host:...,});

  return new Promise(function(resolve,reject) {
    connection.get({'userid':'userid}, function(err,d) {
      if (err) return reject(err);
      return resolve(d);
    });
  })
  .cancellable()
  .catch(Promise.CancellationError,function() {
    connection.close();
  });
}

Hints and tips

  • Check the test folder for usage examples

  • Helper modules are provided in the util directory. Use app.use(require('clues/util/express-clues'))(api) to provide access to clues through an express route.

  • By defining logic object as the Window object, all global functions and variables are made available.

  • Complex trees of "knowledge spaces" can be created with multiple clues objects that refer to one another inside respective logic functions

  • Whenever there is high likelihood of particular information required shortly at some point, the easiest way is to execute an empty user function, with expected requirements as arguments. The logic will appear to have a "clue" for what might happen next.

  • If you want A executed before B, regardless of the results, simply include A as one of the function arguments to B, i.e. logic.B = function(A,Z,callback) { callback(..do something with Z...)}

Keywords

FAQs

Package last updated on 14 May 2015

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc