How to build a chain of methods run and find cycles (collisions) - javascript

What I have: there is some json config (descriptive template), methods stored in diffrent order, its look like:
[
{
"name" : "methodA", //methodA output arguments are methodB input arguments
"inArgs" : "[arg1, arg2]",
"returnArgs" : "[arg3, arg4]"
},
{
"name" : "methodB", //methodB output arguments are methodZ input arguments
"inArgs" : "[arg3, arg5]",
"returnArgs" : "[arg6, arg7]"
},
{
"name" : "methodС",
"inArgs" : "[arg1]",
"returnArgs" : "[arg10]"
},
a bunch of other methods whose input arguments are not part of methodA or methodB
.....
{
"name" : "methodZ",
"inArgs" : "[arg6, arg11]",
"returnArgs" : "[arg20]"
}
]
I need to put these methods in the right order(chain) to run, like:
methodC //the output of this method is not used as an input argument to other methods
methodA //chain i need right order
methodB
methodZ
second case
[
.....
{
"name" : "methodX", //methodX output arguments are methodY input arguments
"inArgs" : «arg1, arg2, arg3]»,
"returnArgs" : «[arg4, arg5, arg6]»
},
{
"name" : "methodY", //methodY output arguments are methodX input arguments
"inArgs" : «[arg4, arg5, arg7]»,
"returnArgs" : «[arg8, arg9, arg10]»
},
....
{
"name" : "methodZ", //methodZ output arguments are methodX input arguments( collision or cycle, so throw error )
"inArgs" : «[arg8, arg11, arg12]»,
"returnArgs" : «[arg3, arg13, arg14]»
},
]
Because the output arguments of one method can be the input arguments of another method (also through a chain of methods of indefinite nesting), it is necessary to catch such collisions, preferably at the stage of parsing the config.
Can someone advise the optimal solution to such a problem, so far only graphs come to mind.
Sorry for my English.

(Sorry, this is a very long answer. I hope it's useful.)
An answer I like
I started trying to solve this using the API you were looking for. I did manage to get something reasonably close. But it wasn't something I would personally use. I rewrote the API and refactored the implementation several times until I came up with something I would like to use. Below I will discuss more of my early steps (which may be more relevant to you) but here is how I would use my version:
const def = {
url: (server, path, query, fragment) => `${server}/${path || ''}${query || ''}${fragment ? `#${fragment}` : ''}`,
query: (parameters) => parameters ? '?' + Object.entries(parameters).map(([k, v]) => `${k}=${v}`).join('&') : '',
server: (schema, port, host) => `${schema}:/\/${host}${port && (String(port) != '80') ? `:${port}` : ''}`,
host: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
}
const vals = {
schema: 'https',
port: '80',
domain: 'example.com',
subdomain: 'test',
path: 'path/to/resource',
parameters: {foo: 42, bar: 'abc'},
fragment: 'baz',
}
runFunctions (def) (vals)
This would generate an output like the following:
{
schema: "https",
port: "80",
domain: "example.com",
subdomain: "test",
path: "path/to/resource",
parameters: {foo: 42, bar: "abc"},
fragment: "baz",
query: "?foo=42&bar=abc",
host: "test.example.com",
server: "https://test.example.com",
url: "https://test.example.com/path/to/resource?foo=42&bar=abc#baz"
}
API Design
The main advantage I see in this version is that the API feels quite clean. The configuration object just maps names to functions and the data object supplied to the resulting function simply maps names to the initial parameters needed by those functions. The result is an enhanced version of that data object. The initial call returns a reusable function. It's all very simple.
Implementation
Some of the history of how I wrote this is embedded in the design. It probably could use a good refactoring; several of the helper functions are probably not necessary. But for now it consists of:
four trivial helper functions:
isEmpty reports whether an array is empty
removeIndex acts like an immutable splice, returning a copy of an array without its nth index
props maps an array of property names to their values in a given object
error simply wraps a string in an error and throws it
one less trivial helper function:
parseArgs retrieves the parameter names from a function. It is based on https://stackoverflow.com/a/9924463. (Oddly, the first one I tried, https://stackoverflow.com/a/31194949, which worked fine in my testing REPL, failed here in the StackOverflow snippets.)
four main functions:
preprocess converts our description object into a configuration object that looks something like the structure described in the question (with name and inArgs properties, although without returnArgs ones.)
makeGraph converts converts a configuration object into an adjacency graph (an array of objects with a name string and a predecessors array of strings.)
sortGraph performs a topological sort on an adjacency graph. It's borrowed from one I wrote at https://stackoverflow.com/a/54408852/, but enhanced with the ability to throw an error if the graph is cyclic.
process accepts the configuration object and the sorted graph and generates a unary function. That function takes a context object and applies the functions in order to the properties of that object, adding a new value to the object keyed to the function name. This calls makeGraph and then sortGraph on the result.
and, finally, a small wrapper function:
runFunctions accepts a description object, calls preprocess on it to create the configuration object, passing that to process and returns the resulting function.
I'm sure there's a reasonable refactoring that removes the need for the intermediate configuration object and/or one that combines the creation and sorting of the graph. That's left as an exercise for the reader!
Complete example
// helpers
const isEmpty = arr =>
arr .length == 0
const removeIndex = (n, arr) =>
arr .slice (0, n) .concat (arr .slice (n + 1) )
const props = (names) => (obj) =>
names .map (name => obj [name] )
const error = (msg) => {
throw new Error (msg)
}
// retrieves parameter named from a function (https://stackoverflow.com/a/9924463)
const parseArgs = (func) => {
var fnStr = func.toString().replace( /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, '');
var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g);
if(result === null)
result = [];
return result;
}
// chooses an appropriate order for our digraph, throwing error on circular
const sortGraph = (
graph,
sorted = [],
idx = graph .findIndex (node => isEmpty (node.predecessors) ),
nodeName = (graph [idx] || {}) .name
) => isEmpty (graph)
? sorted
: idx < 0
? error ('function definitions contains cycle')
: sortGraph (
removeIndex (idx, graph) .map (({name, predecessors}) => ({
name,
predecessors: predecessors .filter (n => n !== nodeName)
}), graph),
sorted .concat (nodeName)
)
// turns a config into an adjacensy graph
const makeGraph = config =>
Object .entries (config) .map (([name, {inArgs}]) => ({
name,
predecessors: inArgs .filter (name => name in config)
}) )
// turns a config object into a function that will run its
// functions in an appropriate order
const process = (config, order = sortGraph (makeGraph (config) )) =>
(vals) =>
order .reduce
( (obj, name) => ({
...obj,
[name]: config [name] .fn .apply (obj, props (config [name] .inArgs) (obj) )
})
, vals
)
// converts simpler configuration into complete version
const preprocess = (def) =>
Object .entries (def) .reduce
( (obj, [name, fn]) => ( { ...obj, [name]: {fn, inArgs: parseArgs(fn)} })
, {}
)
// main function
const runFunctions = (def) =>
process (preprocess (def) )
// input definition
const def = {
url: (server, path, query, fragment) => `${server}/${path || ''}${query || ''}${fragment ? `#${fragment}` : ''}`,
query: (parameters) => parameters ? '?' + Object.entries(parameters).map(([k, v]) => `${k}=${v}`).join('&') : '',
server: (schema, port, host) => `${schema}:/\/${host}${port && (String(port) != '80') ? `:${port}` : ''}`,
host: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
}
// initial input object
const vals = {
schema: 'https',
port: '80',
domain: 'example.com',
subdomain: 'test',
path: 'path/to/resource',
parameters: {foo: 42, bar: 'abc'},
fragment: 'baz',
}
console .log (
runFunctions (def) (vals)
)
Differences from requested design
The API in the question was different: the configuration object looked more like:
[{
name: 'makeUrl',
inArgs: '[domain, subdomain]',
returnArgs: '[host]',
}, /* ... */]
and even after some cleanup, would look like this:
[{
name: 'makeHost',
inArgs: ['domain', 'subdomain'],
returnArgs: ['host'],
}, /* ... */]
This is more flexible than my solution, as it allows multiple returns from a single function, wrapped in an array. But without some uncomfortable gymnastics in the implementation, it would also require multiple returns from each function. Moreover it would require that however you supplied your functions to this, you would have to match the function separately with the name, you would have to ensure that the argument names and order exactly matched the inArgs parameter, and you would have to wrap the more common scalar returns in an array. That might look something like this:
const fns = {
makeHost: (domain, subdomain) => [`${subdomain ? `${subdomain}.` : ''}${domain}`],
/* ... */
}
My Initial Approach
Adding a second configuration parameter and keeping them in sync makes for a much less ergonomic API in my opinion. But it can be done, and it was how I first approached the problem.
This version needed several fewer helper functions. There is no need for preprocess or parseArgs. props was only added to simplify the refactored version above. I haven't checked whether it would help with this one.
Note that process is substantially more complicated here and makeGraph is somewhat more complicated. That's because handling the multiple return arguments adds a fair bit of work. Overall, this version is a few lines shorter than the version above. That's often the trade-off when you create a more comfortable API. But the individual functions are less complex.
Implementation
You can expand this snippet to see a complete example:
// helpers
const isEmpty = arr =>
arr .length == 0
const removeIndex = (n, arr) =>
arr .slice (0, n) .concat (arr .slice (n + 1))
const error = (msg) => {
throw new Error (msg)
}
// chooses an appropriate order for our digraph, throwing error on circular
const sortGraph = (
graph,
sorted = [],
idx = graph .findIndex (node => isEmpty (node.predecessors) ),
nodeName = (graph [idx] || {}) .name
) => isEmpty (graph)
? sorted
: idx < 0
? error ('contains cycle')
: sortGraph (
removeIndex (idx, graph) .map (({name, predecessors}) => ({
name,
predecessors: predecessors .filter (n => n !== nodeName)
}), graph),
sorted .concat (nodeName)
)
// turns a config into an adjacensy graph
const makeGraph = config =>
config .map (({name, inArgs}) => ({
name,
predecessors: inArgs .flatMap (
input => config
.filter ( ({returnArgs}) => returnArgs .includes (input) )
.map ( ({name}) => name )
)
}) )
// main function
const process = (config) => (fns, order = sortGraph (makeGraph (config) )) =>
(vals) =>
order .reduce
( (obj, name) => {
const {inArgs, returnArgs} = config .find
( node => node .name == name
)
const args = inArgs .map (key => obj [key])
const res = fns [name] .apply (obj, args)
return returnArgs .reduce
( (o, k, i) => ({...o, [k]: res [i]})
, obj
)
}
, vals
)
const config = [
{name: 'host', inArgs: ['domain', 'subdomain'], returnArgs: ['host']},
{name: 'server', inArgs: ['schema', 'port', 'host'], returnArgs: ['server']},
{name: 'query', inArgs: ['parameters'], returnArgs: ['query']},
{name: 'url', inArgs: ['server', 'path', 'query', 'fragment'], returnArgs: ['url']}
]
const fns = {
host: (domain, subdomain) => [`${subdomain ? `${subdomain}.` : ''}${domain}`],
server: (schema, port, host) =>
[`${schema}:/\/${host}${port && (String(port) != '80') ? `:${port}` : ''}`],
query: (parameters) => [parameters ? '?' + Object.entries(parameters).map(([k, v]) => `${k}=${v}`).join('&') : ''],
url: (server, path, query, fragment) => [`${server}/${path || ''}${query || ''}${fragment ? `#${fragment}` : ''}`]
}
const vals = {
schema: 'https',
port: '80',
domain: 'example.com',
subdomain: 'test',
path: 'my/path',
parameters: {foo: 42, bar: 'abc'},
fragment: 'baz',
}
console .log (
process (config) (fns) (vals)
)
Intermediate Work
I wouldn't even attempt to show all the stages my code went through between the initial and final versions, but there was an interesting waypoint in the API, in which I used a configuration object like this:
const config = {
host: {
inArgs: ['domain', 'subdomain'],
fn: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
},
/* ... */
}
There is something to be said for that version: it avoids the need to parse the function in order to to get the parameters. The variety of fragile answers to How to get function parameter names/values dynamically? demonstrate that this is a non-trivial problem. And it should be quite familiar to users of Angular's dependency injection.
But in the end, this is just too much cleaner:
const config = {
host: fn: (domain, subdomain) => `${subdomain ? `${subdomain}.` : ''}${domain}`,
/* ... */
}
And hence I prefer my final version.
Conclusions
This is a non-trivial problem.
The implementation is not particularly difficult in any of these versions. But breaking it down into the useful pieces is challenging. And determining a useful API when we're afforded the flexibility to choose whatever seems right can take a lot of thought, a lot of discussion, and a lot of playing around.
Different developers will make different choices, often for important reasons, but to me sacrificing the likely rare facility to have multiple returns from a single function was entirely worth it to achieve a substantially simpler configuration object. In fact, it's difficult to imagine a simpler configuration.

An easier, but not-that-bulletproof (you cannot detect cycles) solution would be to wrap every value into a Promise: When a function generates certain outputs, resolve the Promise, then use Promise.all on the inputs. That way, the promises will automatically determine the right order:
const context = { /* [var: string]: { resolve(v: T), value: Promise<T> */ };
function getVar(name) {
if(context[name]) return context[name].value;
const cont = context[name] = { };
return cont.value = new Promise(res => cont.resolve = res);
}
function setVar(name, value) {
getVar(name); // Make sure prop is initialized, don't await!
context[name].resolve(value);
}
async function run(fn, argNames, resultNames) {
const args = await Promise.all(argNames.map(getVar));
const results = fn(...args);
for(let i = 0; i < results.length; i++)
setVar(resultNames[i], results[i]);
}

Related

JavaScript - Creating Object with specific format out of 2 Arrays

I have 2 Arrays.
The Array "people" just holds basic information of some people.
The Array "subscriptions" has a lot of different subscriptions to games.
I want to have have an Array, where I can sort of have an Overview of what game subscriptions each person has. This is not real code I use, I am just trying to get used to JavaScript.
an example Element of an Array called people:
{"number":4251,"name":"Alex","surname":"Scott"}
an example Element of an Array called subscriptions:
{"number":4329,game:"Tetris"}
I want to make a new new Array with the following format:
person: (people[i]), subscriptions: [(subscriptions[j], subscriptions[j+k], ...)]
What I tried:
const array3 = people.map(x => {"person": x , "subscriptions": subscriptions.filter(y => y.number === x.number)});
It get this Error:
SyntaxError: Unexpected token :
How can I insert multiple key value pairs in in these Objects?
This happens because your argument in the map is interperting the { as opening bracket of the method body.
const arr = [{some: 1, object: 2}, {some:3, object:4}]
arr.map((o) => {original: o, cool: 'yes'})
To resolve this you have to explicitly return the object or add additional parenthesis to let the interperter know that this is not the method body:
const arr = [{some: 1, object: 2}, {some:3, object:4}]
const mapped = arr.map((o) => {return {original: o, cool: 'yes'}})
console.log(mapped)
const arr = [{some: 1, object: 2}, {some:3, object:4}]
const mapped = arr.map((o) => ({original: o, cool: 'yes'}))
console.log(mapped)
I hope this helps!
In your original code there is a syntax error, when it comes to the mapping part. Either you go with the long version of this command or, since you directly want to return the element, you can use the short version:
const people = [{"number":4251,"name":"Alex","surname":"Scott"}, {"number": 4329, "name":"Mr", "surname": "Tetri"}]
const subscriptions = [{"number":4329,game:"Tetris"}, {"number":4329, game:"Solitaire"}, {number: 4251, game: "Tetris"}]
// using an anonymous function including full body
const arrayLongVersion = people.map(x => { return {"person": x , "subscriptions": subscriptions.filter(y => y.number === x.number)} });
// using an anonymous function with direct return of elements
const arrayShortVersion = people.map(x => ({person: x , subscriptions: subscriptions.filter(y => y.number === x.number)}))
Here you go
const people = [
{"number":1,"name":"Alex","surname":"Scott"},
{"number":2,"name":"John","surname":"Anderson"},
{"number":3,"name":"Jacob","surname":"Sanderson"},
{"number":4,"name":"Lionel","surname":"Messi"},
{"number":5,"name":"Cristiano","surname":"Ronaldo"}
];
const subscriptions = [
{"number":1,game:"Tetris"},
{"number":2,game:"Super Mario"},
{"number":3,game:"Fortnite"},
{"number":4,game:"FIFA"},
{"number":5,game:"Zelda"}
];
const peopleSubscriptions = people.map(person => {
return {
...person,
subscriptions: subscriptions.filter(sub => sub.number === person.number)
}
});
console.log(peopleSubscriptions);

Can i spread an Array of parameter names into a function declaration

I have an object which returns,
[
{
name:"getSpeed",
params:["distance","time"]
},
{
name:"getTime",
params:["speed","distance"]
},
...
]
This object is subject to change as it is gathered from an embedded device.
im trying to convert this into an object with callable functions i.e.
let myObj = {
getSpeed: function(distance, time){
/* do something (this is irrelevant) */
},
getTime: function(speed, distance){
/* do something (again not relevant) */
}
}
is there any way to map an array of strings to function parameters when mapping over an array?
According to your comments it appears you want to create functions from this definition which send commands with a function-call-like string which is evaled on the other side, and you don't actually care about the paramater names but rather about the correct number of parameters.
I would therefore recommend something like this:
const myObj = Object.fromEntries(data.map(({ name, params }) => [
name,
(...args) => {
if (args.length !== params.length) {
throw new TypeError(`${name} expected ${params.length} arguments, got ${args.length}`)
}
this.UART.write(`${name}(${args.map(arg => JSON.stringify(arg)).join(',')})`)
}
]))
This will work with all the datatypes that JSON supports, and will as a side effect also pass undefined as argument correctly.
Here is a runnable example with console.log instead of this.UART.write:
const data = [
{
name: "getSpeed",
params: ["distance", "time"]
},
{
name: "getTime",
params: ["speed", "distance"]
}
]
const myObj = Object.fromEntries(data.map(({ name, params }) => [
name,
(...args) => {
if (args.length !== params.length) {
throw new TypeError(`${name} expected ${params.length} arguments, got ${args.length}`)
}
console.log(`${name}(${args.map(arg => JSON.stringify(arg)).join(',')})`)
}
]))
myObj.getSpeed(123, 456) // prints `getSpeed(123,456)`
myObj.getTime(123, 456) // prints `getTime(123,456)`
myObj.getTime("hello", true) // prints `getTime("hello",true)`
myObj.getTime(1) // throws `getTime expected 2 arguments, got 1`
However, as you said yourself, the whole eval business is not ideal anyway. I would recommend - if possible - to reconsider the protocol to use something more secure and robust like gRPC or, one layer below, protocol buffers. Given that you are using JavaScript on both ends, JSON-RPC could also be a nice solution.

graphql passing dynamic data to mutation

haven't used graphql or mongodb previously. What is the proper way to pass objects for the update mutation?
Since the only other way i see to pass multiple dynamically appearing parameters is to use input type which is appears to be a bit ineffective to me (in terms of how it looks in the code, especially with bigger objects), i just pass the possible values themselves. however in this case i need to dynamically construct updateObject, which again, going to get messy for the bigger models.
for example now i did:
Mutation: {
updateHub: async (_, { id, url, ports, enabled }) => {
const query = {'_id': id};
const updateFields = {
...(url? {url: url} : null),
...(ports? {ports: ports} : null),
...(enabled? {enabled: enabled} : null)
};
const result = await HubStore.findByIdAndUpdate(query, updateFields);
return {
success: !result ? false : true,
message: 'updated',
hub: result
};
}
}
any advise on the better way to handle this?
thanks!
It appears your code could benefit from using ES6 spread syntax -- it would permit you to deal with an arbitrary number of properties from your args object without the need for serial tertiary statements.
Mutation: {
updateHub: async (_, { id, ...restArgs } ) => {
const query = {'_id': id};
const updateFields = { ...restArgs };
const result = await HubStore.findByIdAndUpdate(query, updateFields);
return {
success: !result ? false : true,
message: 'updated',
hub: result
};
}
}
If for some reason you need to explicitly set the undefined properties to null in your object, you could possibly use some a config obj and method like defaults from the lodash library as shown below:
import { defaults } from 'lodash';
const nullFill = { url: null, ports: null, enabled: null }; // include any other properties that may be needed
Mutation: {
updateHub: async (_, { id, ...restArgs } ) => {
const query = {'_id': id};
const updateFields = defaults(restArgs, nullFill);
const result = await HubStore.findByIdAndUpdate(query, updateFields);
return {
success: !result ? false : true,
message: 'updated',
hub: result
};
}
}
Also, FWIW, I would consider placing the dynamic arguments that could be potentially be updated on its own input type, such as HubInput in this case, as suggested in the graphql docs. Below I've shown how this might work with your mutation. Note that because nothing on HubInput is flagged as requird (!) you are able to pass a dynamic collection of properties to update. Also note that if you take this appraoch you will need to properly destructure your args object initially in your mutation, something like { id, input }.
input HubInput {
url: String
ports: // whatever this type is, like [String]
enabled: Boolean
// ...Anything else that might need updating
}
type UpdateHubPayload {
success: Boolean
message: String
hub: Hub // assumes you have defined a type Hub
}
updateHub(id: Int, input: HubInput!): UpdateHubPayload

How can i use ramda.js with this code?

I am beginning to use ramda, but have doubts about how to implement functions ramda.
This code is to mount a select object to do queries for sequelize.
See code:
const stringRequired = string => !!string.length
const numberRequired = number => Number.isInteger(number)
const validadeInput = (value, initial, validade) => (
validade(value) ? value : initial
)
const input = (obj, key, initial, validade) => (
typeof obj[key] !== 'undefined' ?
validadeInput(obj[key], initial, validade) :
initial
);
const addValue = (obj, key, value) => {
const prop = {}
prop[key] = value
return Object.assign(obj, prop)
}
const addFilter = (obj, key, value, validate) => (
validate(value) ? addValue(obj, key, value)
: obj
)
const selector = (query = {}) => {
const limit = input(query, 'limit', 10, numberRequired);
const name = input(query, 'name', '', stringRequired);
let select = {}
select = addFilter(select, 'name', name, stringRequired);
select = addFilter(select, 'limit', limit, numberRequired);
return select
}
console.log(selector());
// { limit: 10 }
console.log(selector({ name: 'David Costa' }));
// { limit: 10, name: 'David Costa' }
console.log(selector({ limit: 50 }));
// { limit: 50 }
Or see demo on link
http://jsbin.com/zerahay/edit?js,console,output
Why?
I think you need to consider why you want to convert this to Ramda
(Disclaimer: I'm a Ramda author) Ramda is a library, a toolkit. Use it when it helps clean up your code or when it makes it easier to understand the problem and its solution. Don't use it when it doesn't.
That said, I did refactor it, using Ramda:
A Refactoring
I simply tried to refactor it a bit. In the end, I replaced all your helper functions with a single one that takes a list of conditions such as ['limit', 10, numberRequired] to create a function equivalent to your selector.
I did use a few Ramda functions along the way, but the only one that offers substantive help is assoc, which creates a new object from an old one and a key and value. Using, for instance, compose(Boolean, length) is cleaner than const stringRequired = string => !!string.length, but it's not a large difference.
The important change, to my mind, is the makeSelector function, which makes creating your selector function much more declarative. It's a bit ugly, and I probably would write it differently if I were starting from scratch, but I did this in a series of steps, inlining your helper functions until I had a reasonably short function that had the same behavior.
// rules
const stringRequired = R.compose(Boolean, R.length)
const numberRequired = number => Number.isInteger(number)
// utils
const makeSelector = (conditions) => (query = {}) => R.reduce(
(select, [key, initial, validate]) => {
const value = key in select && validate(select[key]) ? select[key] : initial;
return validate(value) ? R.assoc(key, value, select) : select
},
query,
conditions
)
// main
const selector = makeSelector([
['limit', 10, numberRequired],
['name', '', stringRequired]
])
console.log(selector());
console.log(selector({ name: 'David Costa' }));
console.log(selector({ limit: 50 }));
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.js"></script>
I'm a little hesitant to simply respond with a translated block of code and my recommendation for learning going forward would be to try replace the functions you already have one by one with those from Ramda.
Hesitation aside, the following offers one example of what your code might look like when using various functions from Ramda.
// The existing check for `!!string.length` allows for potential issues with
// arrays being passed in and incorrectly validating, so this will ensure the
// value is indeed a string before checking its length
const stringRequired = R.both(R.is(String), R.complement(R.isEmpty))
// `addFilter` produces a function that takes an object and uses the provided
// function to validate the value associated with the provided key if it exists
// otherwise using the provided `initial` value. If valid, an object containing
// the key and value will be returned, otherwise an empty object is returned.
const addFilter = (validate, initial, key) =>
R.pipe(
R.propOr(initial, key),
R.ifElse(validate, R.objOf(key), R.always({}))
)
// `selector` takes an object and passes it to each function generated by
// calling `addFilter`, merging the resulting objects together.
const selector = (q = {}) =>
R.converge(R.merge, [
addFilter(stringRequired, '', 'name'),
addFilter(Number.isInteger, 10, 'limit')
])(q)
console.log(selector())
console.log(selector({ name: 'David Costa' }))
console.log(selector({ limit: 50 }))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js"></script>

Angular 2, ngrx/store, RxJS and tree-like data

I've been trying to figure out a way to use the select operator in combination with rxjs's other operators to query a tree data structure (normalized in the store to a flat list) in such a way that it preserves referential integrity for ChangeDetectionStrategy.OnPush semantics but my best attempts cause the entire tree to be rerendered when any part of the tree changes. Does anyone have any ideas?
If you consider the following interface as representative of the data in the store:
export interface TreeNodeState {
id: string;
text: string;
children: string[] // the ids of the child nodes
}
export interface ApplicationState {
nodes: TreeNodeState[]
}
I need to create a selector that denormalizes the state above to return a graph of objects implementing the following interface:
export interface TreeNode {
id: string;
text: string;
children: TreeNode[]
}
That is, I need a function that takes an Observable<ApplicationState> and returns an Observable<TreeNode[]> such that each TreeNode instance maintains referential integrity unless one of its children has changed.
Ideally I'd like to have any one part of the graph only update its children if they've changed rather than return an entirely new graph when any node changes.
Does anyone know how such a selector could be constructed using ngrx/store and rxjs?
For more concrete examples of the kinds of things I've attempted check out the snippet below:
// This is the implementation I'm currently using.
// It works but causes the entire tree to be rerendered
// when any part of the tree changes.
export function getSearchResults(searchText: string = '') {
return (state$: Observable<ExplorerState>) =>
Observable.combineLatest(
state$.let(getFolder(undefined)),
state$.let(getFolderEntities()),
state$.let(getDialogEntities()),
(root, folders, dialogs) =>
searchFolder(
root,
id => folders ? folders.get(id) : null,
id => folders ? folders.filter(f => f.parentId === id).toArray() : null,
id => dialogs ? dialogs.filter(d => d.folderId === id).toArray() : null,
searchText
)
);
}
function searchFolder(
folder: FolderState,
getFolder: (id: string) => FolderState,
getSubFolders: (id: string) => FolderState[],
getSubDialogs: (id: string) => DialogSummary[],
searchText: string
): FolderTree {
console.log('searching folder', folder ? folder.toJS() : folder);
const {id, name } = folder;
const isMatch = (text: string) => !!text && text.toLowerCase().indexOf(searchText) > -1;
return {
id,
name,
subFolders: getSubFolders(folder.id)
.map(subFolder => searchFolder(
subFolder,
getFolder,
getSubFolders,
getSubDialogs,
searchText))
.filter(subFolder => subFolder && (!!subFolder.dialogs.length || isMatch(subFolder.name))),
dialogs: getSubDialogs(id)
.filter(dialog => dialog && (isMatch(folder.name) || isMatch(dialog.name)))
} as FolderTree;
}
// This is an alternate implementation using recursion that I'd hoped would do what I wanted
// but is flawed somehow and just never returns a value.
export function getSearchResults2(searchText: string = '', folderId = null)
: (state$: Observable<ExplorerState>) => Observable<FolderTree> {
console.debug('Searching folder tree', { searchText, folderId });
const isMatch = (text: string) =>
!!text && text.search(new RegExp(searchText, 'i')) >= 0;
return (state$: Observable<ExplorerState>) =>
Observable.combineLatest(
state$.let(getFolder(folderId)),
state$.let(getContainedFolders(folderId))
.flatMap(subFolders => subFolders.map(sf => sf.id))
.flatMap(id => state$.let(getSearchResults2(searchText, id)))
.toArray(),
state$.let(getContainedDialogs(folderId)),
(folder: FolderState, folders: FolderTree[], dialogs: DialogSummary[]) => {
console.debug('Search complete. constructing tree...', {
id: folder.id,
name: folder.name,
subFolders: folders,
dialogs
});
return Object.assign({}, {
id: folder.id,
name: folder.name,
subFolders: folders
.filter(subFolder =>
subFolder.dialogs.length > 0 || isMatch(subFolder.name))
.sort((a, b) => a.name.localeCompare(b.name)),
dialogs: dialogs
.map(dialog => dialog as DialogSummary)
.filter(dialog =>
isMatch(folder.name)
|| isMatch(dialog.name))
.sort((a, b) => a.name.localeCompare(b.name))
}) as FolderTree;
}
);
}
// This is a similar implementation to the one (uses recursion) above but it is also flawed.
export function getFolderTree(folderId: string)
: (state$: Observable<ExplorerState>) => Observable<FolderTree> {
return (state$: Observable<ExplorerState>) => state$
.let(getFolder(folderId))
.concatMap(folder =>
Observable.combineLatest(
state$.let(getContainedFolders(folderId))
.flatMap(subFolders => subFolders.map(sf => sf.id))
.concatMap(id => state$.let(getFolderTree(id)))
.toArray(),
state$.let(getContainedDialogs(folderId)),
(folders: FolderTree[], dialogs: DialogSummary[]) => Object.assign({}, {
id: folder.id,
name: folder.name,
subFolders: folders.sort((a, b) => a.name.localeCompare(b.name)),
dialogs: dialogs.map(dialog => dialog as DialogSummary)
.sort((a, b) => a.name.localeCompare(b.name))
}) as FolderTree
));
}
If willing to rethink the problem, you could use Rxjs operator scan:
If no previous ApplicationState exists, accept the first one. Translate it to TreeNodes recursively. As this is an actual object, rxjs isn't involved.
Whenever a new application state is received, ie when scan fires, implement a function that mutates the previous nodes using the state received, and returns the previous nodes in the scan operator. This will guarantee you referential integrity.
You might now be left with a new problem, as changes to mutated tree nodes might not be picked up. If so, either look into track by by making a signature for each node, or consider adding a changeDetectorRef to a node (provided by component rendering node), allowing you to mark a component for update. This will likely perform better, as you can go with change detection strategy OnPush.
Pseudocode:
state$.scan((state, nodes) => nodes ? mutateNodesBy(nodes, state) : stateToNodes(state))
The output is guaranteed to preserve referential integrity (where possible) as nodes are built once, then only mutated.

Categories

Resources