Search code examples
javascriptreactive-programmingtransducercujojs

Create a pipeline from Json to streams with transducers-js and most.js


I have this Amd module

define(function (require, exports, module) {
'use strict';
var t = require("transducers");
var most = require("most");

var groupby = function (prev, curr) {
       var key = curr.type;
       if (!prev[key]) {
             prev[key] = [curr];
       } else {
            prev[key].push(curr);
       }
       return prev;
};
function category(kv) {
     return {
           type: kv[0],
           label: kv[1][0].label,
           counter: kv[1].length
     }
  }
  function dom(cat) {
       var el = document.createElement("li");
       el.innerHTML = cat.label;
       return el;
  }

function append(prev, curr) {
        prev.appendChild(curr);
        return prev;
 }

function createClick(prev, curr) {
     return prev.merge(most.fromEvent("click", curr)
      .map(function (e) {
            return e.currentTarget.innerHTML;
      })
    )
 }

var xf = t.comp(
                 t.map(category),
                 t.map(dom)
               );

module.exports = {
         main: function (data) {

               var groups = t.reduce(groupby, {}, data);
               var container = t.transduce(xf, append, document.querySelector("ul"), groups);
               var streams = t.reduce(createClick, most.empty(), [].slice.call(container.querySelectorAll("li"), 0));

              streams.forEach(function (e) {
                   console.log("click", e);
               });
        }
     };
});

Main function takes a list of items, then groups them by 'type' property. After that it creates and appends < li > elements. Finally it creates a stream of clicks. I'm new in reactive programming and transducers.

But I was wondering if there would be a way to create a pipeline.

I trouble because groupby is a reduction and a can't compose it in transducer. I'm sure I'm missing something. Thanks


Solution

  • Try and separate your problem into things that can operate on the individual item vs on the collection and wait until last to reduce them. also check into the often missed "scan" operation which can save you from over aggressive reductions

    In your example, you have 3 reducing possible operations listed: - merge all click streams into one stream of events - merge all dom into a single ul - count

    the can all be accomplished with scan, but the issue arrises in that you want to unique categories, but you also count the non unique ones. It's not clear from your example if that's actually a requirement though...

    Since most already works similar to transducers, you don't really need them. For this example I'll stick with pure mostjs;

    var most = require("most");
    
    function gel(tag) {
        return document.createElement(tag);
    }
    
    var cache = Object.create(null);
    
    function main(dataArray) {
        most.from(dataArray)
    //only pass along unique items to represent categories
    //optionally, also count...
            .filter(function uniq(item) {
                var category = item.type;
                if (!(category in cache))
                    cache[category] = 0;
                cache[category]++;
                return cache[category] == 1;
            })
    //transform
            .map(function makeLI(item) {
                var li = gel("li");
                li.innerHTML = item.label;
                item.dom = li;
            })
    //attach click handler
            .map(function (item) {
                item.click = most
                    .fromEvent("click", item.dom)
                    .map(function(e) {
                        item.label;
                    });
                return item;
            })
    // merge
            .scan(function mergeIn(all, item) {
                el.appendChild(item.dom);
                clicks.merge(item.click);
            }, { ul: gel("ul"), clicks: most.empty() })
    //force stream init b creating a demand
            .drain()
    //most resolve stream into promises, but we could have just 
    //as easily used reduce here instead
            .then(function onComplete(all) {
                all.clicks.forEach(function(msg) {
                    console.log("click", e);
                })
            })
    }
    

    further variation are possible. for example if we wanted to add a sublist for each category item, we could attach a stream to the context object for each category and incrementally append to each child as we go