3

I'm use event emitters as synchronization primitives. For example I have one class which asks semaphore like structure in Redis. If semaphore is set it emits an event. The code is listed bellow:

var redis = require("redis"),
    async = require('async'),
    client = redis.createClient(),
    assert = require('assert'),
    EventEmitter = require('events').EventEmitter;
    util = require('util');
var isMaster = false,
    SEMAPHORE_ADDRESS = 'semaphore';
var SemaphoreAsker = function() {
  var self = this;
  var lifeCycle = function (next) {
    client.set([SEMAPHORE_ADDRESS, true, 'NX', 'EX', 5], function(err, val) {
      console.log('client');
      if(err !== null) { throw err; }
      if(val === 'OK') {
        self.emit('crown');
      } else {
        console.log('still a minion');
      }
    });
  };
  async.forever(
    function(next) {
      setTimeout(
        lifeCycle.bind(null, next),
        1000
      );
    }
  );
};
util.inherits(SemaphoreAsker,  EventEmitter);
(new SemaphoreAsker()).on('crown', function() {
  console.log('I`m master');
});

It works but looks a little bit heavy. Is it possible to rewrite the example with BaconJS(RxJS/whateverRPlibrary)?

kharandziuk
  • 12,020
  • 17
  • 63
  • 121
  • 1
    Cool question. I'll post an RxJs answer for comparison when I get home tonight. I think it is quite a bit shorter than the posted bacon answer. – Brandon May 21 '15 at 00:11

3 Answers3

4

The following should work in RXJS:

var callback = Rx.Observable.fromNodeCallback(client.set, client);

var source = Rx.Observable.interval(1000)
.selectMany(function() {
  return callback([SEMAPHORE_ADDRESS, true, 'NX', 'EX', 5]);
})
.filter(function(x) { return x === 'OK'; })
.take(1);


source.subscribe(function(x) {
  console.log("I am master");
});

If you are willing to additionally include the rx-node module you can also keep your event emitter structure by using

var emitter = RxNode.toEventEmitter(source, 'crown');

emitter.on('crown', function(){});
emitter.on('error', function(){});
emitter.on('end', function(){});
paulpdaniels
  • 18,395
  • 2
  • 51
  • 55
  • 1
    yeah that's pretty much what I was going to post, except: `callback = Rx.Observable.fromNodeCallback(client.set, client);` instead of using `fromCallback`. [fromNodeCallback](https://github.com/Reactive-Extensions/RxJS/blob/master/doc/api/core/operators/fromnodecallback.md) takes care of the error handling for you. – Brandon May 21 '15 at 02:39
  • Nice call @Brandon, I updated the example accordingly. – paulpdaniels May 21 '15 at 05:43
2

I used the basic Bacon.fromBinder to create a custom stream for this. Without a working example this is a bit of guesswork, but hopefully this helps you.

var redis = require("redis"),
    client = redis.createClient(),
    assert = require('assert'),
    Bacon = require('bacon');

var SEMAPHORE_ADDRESS = 'semaphore';

var SemaphoreAsker = function() {
  return Bacon.fromBinder(function (sink) {
    var intervalId = setInterval(pollRedis, 1000)

    return function unsubscribe() {
      clearInterval(intervalId)
    }

    function pollRedis() {
      client.set([SEMAPHORE_ADDRESS, true, 'NX', 'EX', 5], function(err, val) {
        if(err !== null) { sink(new Bacon.Error(err)) }
        else if(val === 'OK') { sink(new Bacon.Next('crown'))
        else { assert.fail(); }
      }
    }
  })
}

SemaphoreAsker().take(1).onValue(function() {
  console.log("I am master")
})
OlliM
  • 7,023
  • 1
  • 36
  • 47
  • Thanks for a great answer! What does take(1) do in this case? Does it stop the stream after first value? – kharandziuk May 20 '15 at 08:07
  • Now, I understand. There is the unsubscribe function and it will call it after the first value – kharandziuk May 20 '15 at 08:13
  • 1
    Exactly, we're only interested in one "OK" value. After the first value the stream created with `Bacon.fromBinder` has no more subscribers left, so the unsubscribe function is called. – OlliM May 20 '15 at 09:40
  • Bacon takes care of unsubscribing, usually you don't even need to write any unsubscribe logic (I create the stream manually here, so I have to write the unsub logic as well). The main point from memory management point of view is to remember to use `take(...)`, `takeUntil(...)` etc. so that the stream ends when its values are no longer needed. Especially important if you use a `Bacon.Bus`. – OlliM May 20 '15 at 09:42
  • Hi, look at the my answer. It mimics @paulpdanies answer. What do you about it? – kharandziuk May 21 '15 at 10:19
2

The @paulpdanies answer but rewritten with Bacon:

var source = Bacon.interval(1000).flatMap(function() {
    return Bacon.fromNodeCallback(
        client, 'set', [SEMAPHORE_ADDRESS, true, 'NX', 'EX', 1]
    );
})
.filter(function(x) { return x === 'OK'; })
.take(1);

source.onValue(function(x) {
    console.log(x);
});
kharandziuk
  • 12,020
  • 17
  • 63
  • 121
  • 1
    This looks fine, you can use `Bacon.fromNodeCallback(client, 'set', [...])` to avoid explicit binding – OlliM May 21 '15 at 11:28