Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simulating a command queue and undo stack with RxJS

I'm attempting to replicate this demo using RxJS. The demo is a small application, where the user controls a robot. The robot can move forwards or backwards, rotate left or right, and pick up or drop an item. The user can queue commands (such as "Forward", "rotate"), and the commands in the queue are executed when the user clicks on the "Execute"-button. The user can also undo commands that have been already executed.

Traditionally this application would be quite easy to implement using a queue for the commands that have not been executed yet. Executed commands are pushed into a stack, and whenever the undo-button is pressed, the top command is popped and undone.

I'm able to "collect" the commands and execute them by doing this:

var id = 0;
var add = Rx.Observable.fromEvent($("#add"), 'click').map(function(){
  var ret = "Command_"+id;
  id++;
  return ret
})
var invoke = Rx.Observable.fromEvent($("#invoke"), 'click')
var invokes = add.buffer(invoke)

The buffer() method transforms the stream into a stream of arrays. I can either subscribe to the invokes stream and get arrays of commands:

invokes.subscribe(function(command_array){...})

or I can create a Rx.Subject() where I just push the commands one by one:

var invoked_commands = new Rx.Subject()
invokes.subscribe(function(command_array){
  for(var i=0; i < command_array.length; i++){
    invoked_commands.onNext(command_array[i])
  }
});

invoked_commands.subscribe(function(command){ ...});

To be honest I have no idea which approach would be better, but I then again I don't know if that's even too relevant to me right now. I've been trying to figure out how to implement the undo functionality, but I have absolutely no idea how to do it.

In my mind it would have to be something like this (sorry for the formatting):

-c1---c2-c3--------->

----------------u---u-> ("u" = clicking the undo button)

----------------c3--c2> (get commands from newest to oldest, call the undo() method)

So my question is twofold:

  1. Is my approach of collecting the commands good?
  2. How can I implement the undo feature?

EDIT: I'm comparing transformative and reactive styles, and I'm implementing this demo using both. Therefore I'd like to stick to using Rx* features as much as possible.

like image 562
T.Kaukoranta Avatar asked Sep 28 '22 11:09

T.Kaukoranta


1 Answers

You have to go ahead and maintain state for the undo stack. I think your approach to collecting commands is reasonable. If you keep your Subject you can sort of decouple the undo functionality from the command execution by making another subscription to the subject:

var undoQueue = [];
invoked_commands.subscribe(function (c) { undoQueue.unshift(c); });
Rx.Observable
    .fromEvent($("#undo"), "click")
    .map(function () { return undoQueue.pop(); })
    .filter(function (command) { return command !== undefined; })
    .subscribe(function (command) { /* undo command */ });

Edit: Using only Rx without a mutable array. This seems unnecessarily convoluted, but oh well, it is functional. We use scan to maintain the undo queue, and emit a tuple with the current queue along with whether a undo command should be executed. We merge executed commands with undo events. Execute commands add to the queue, undo events pop from the queue.

var undo = Rx.Observable
    .fromEvent($("#undo"), "click")
    .map(function () { return "undo"; });
invoked_commands
    .merge(undo)
    .scan({ undoCommand: undefined, q: [] }, function (acc, value) {
        if (value === "undo") {
            return { undoCommand: acc.q[0], q: acc.q.slice(1) };
        }

        return { undoCommand: undefined, q: [value].concat(acc.q) };
     })
     .pluck("undoCommand")
     .filter(function (c) { return c !== undefined })
     .subscribe(function (undoCommand) { ... });
like image 146
Brandon Avatar answered Oct 19 '22 17:10

Brandon