3

Let's say I have a server that sends data to its clients each time a client's data has changed. The client class looks like this:

function Client() {
    this.data = {};
    this.update = function (key, value) {
        this.data[key] = value;
        this.emitUpdate();
    };

    this.emitUpdate = function () {
        // tell server to send this client's data
    };
}

var myClient = new Client();

If I change just one thing:

myClient.update("name", "John");

the server updates all clients with this client's new information. Great.

But...

If I change multiple things at once from different places in my app:

if (something === true) {
    myClient.update("something", true);
} else {
    myClient.update("something_else", true);
}

myClient.update("age", Date.now());

there will always be two things changed and emitUpdate() will be called twice. The server will send the data twice and all clients will have to render it twice. Imagine 100 changes like that occurring... It would be a big overhead, considering the server could send the update just once, since all of these changes happen in one time frame.

How do I make emitUpdate() invoke only once per call stack? I use underscore.js and checked out the defer() and throttle() functions. Problem is, I need their effects combined.

var log = function () {
  console.log("now");
};

// logs "now" 10 times
for (let i = 0; i < 10; i++) {
  _.defer(log);
}

// logs "now" 10 times
var throttled = _.throttle(log, 0);
for (let i = 0; i < 10; i++) {
  throttled();
}

If I use throttle() with a different amount of wait like 1 or 2, the results are inconsistent - sometimes it's called once, sometimes more.

I need something like this:

// logs "now" once
var magic = _.deferottle(log);
for (let i = 0; i < 10; i++) {
  magic();
}

Is there a way I could achieve it?

dodov
  • 5,206
  • 3
  • 34
  • 65
  • Why not just separate `emitUpdate` from `update` and call the former as needed? – hindmost Feb 27 '17 at 19:32
  • Why not debounce on the end of updates to emit a change? http://stackoverflow.com/questions/25991367/difference-between-throttling-and-debouncing-a-function http://underscorejs.org/#debounce – corn3lius Feb 27 '17 at 19:37

3 Answers3

3

This should work for multiple synchronous calls to the update() method:

function Client() {
  this.data = {};
  this.update = function (key, value) {
    this.data[key] = value;
    if (!this.aboutToUpdate) {
      this.aboutToUpdate = setTimeout(() => {
        this.aboutToUpdate = false
        this.emitUpdate()
      })
    }
  };

  this.emitUpdate = function () {
    // tell server to send this client's data
    console.log('update sent', this.data)
  };
}

// This should only log 'update sent' once
var client = new Client()
for (var i = 0; i < 5; i++) {
  client.update(i, i)
}
idbehold
  • 16,833
  • 5
  • 47
  • 74
2

Actually you don't need throttle, just use defer in conjunction with some boolean flag check.

Something like this:

this.update = function (key, value) {
    this.data[key] = value;
    if (this.isQueued) return;
    this.isQueued = true;
    _.defer(function () {
        this.isQueued = false;
        this.emitUpdate();
    }.bind(this));
};
hindmost
  • 7,125
  • 3
  • 27
  • 39
  • You realize that this is literally the same as my answer, except with a dependency on underscore and 14 minutes later. – idbehold Feb 27 '17 at 20:22
0

A solution popped in my head while taking a shower:

var deferottle = function(func) {
  var called, args, ctx;

  return function() {
    args = arguments;
    ctx = this;

    if (!called) {
      called = true;
      setTimeout(function() {
        func.apply(ctx, args);
        called = false;
      });
    }
  };
};

var magic = deferottle(function(num) {
  console.log("num: ", num);
});

for (let i = 0; i < 10; i++) {
  magic(i);
}

Turns out debounce() with a wait of 0 can also be used:

var magic = _.debounce(function(num) {
  console.log("num: ", num);
}, 0);

for (let i = 0; i < 10; i++) {
  magic(i);
}
<script src="http://underscorejs.org/underscore-min.js"></script>
dodov
  • 5,206
  • 3
  • 34
  • 65
  • 1
    This isn't really a solution to your problem since it will only propagate the last call. For example `myClient.update("something_else", true); myClient.update("age", Date.now());` will only update the `age` key. – idbehold Feb 27 '17 at 20:25
  • Yeah, it doesn't work for the first part of my answer, but it is a solution to the second part. The implementation of underscore's `debounce()`, is something like my `deferottle()` function, actually. Thanks for pointing this out, though! – dodov Feb 27 '17 at 20:31
  • I meant the first part of my _question_. And I have an extra comma. – dodov Feb 27 '17 at 21:02