Friday, March 16, 2012

Handle Dart Websockets Once

‹prev | My Chain | next›

Aw, man! I got my simple comic book app working with the Hipster MVC library and Dart websockets. It can do all sorts of CRUD and it actually works. Except that I keep seeing errors like:


The first batch of red is from the first comic book that I delete over web sockets. The second batch of red is from the second comic book that I delete. The second batch actually has two stacktraces where the first only has the one. So my woes are compounding with each websocket operation. Ugh.

The error message itself indicates that the completer inside my custom data sync has already completed:
wsSync(method, model) {
  final completer = new Completer();

  String message = "$method: ${model.url}";
  if (method == 'delete') message = "$method: ${model.id}";
  if (method == 'create') message = "$method: ${JSON.stringify(model.attributes)}";

  print("sending: $message");
  ws.send(message);

  // Handle messages from the server, completing the completer
  ws.
    on.
    message.
    add((event) {
      print("The data in the event is: " + event.data);
      completer.complete(JSON.parse(event.data));
    });

  return completer.future;
}
Given that the number of completers already completed increments by one with each websocket operation, it does not take me long to deduce the source of the problem: the old listeners (e.g. from the initial fetch, and the first delete) are still listening for messages on the websocket event list. Each time my custom wsSync() function is called, a new listener is added. No listener is ever removed—even after it has handled the one and only response from the server that it is meant to handle.

First up, I try to prevent the other listeners from firing. I know that this is not an ideal solution, but it would be a starting point. Unfortunately, this has no effect:
  ws.
    on.
    message.
    add((event) {
      print("The data in the event is: " + event.data);
      completer.complete(JSON.parse(event.data));

      event.preventDefault();
      event.stopImmediatePropagation();
    });
Various combinations of preventDefault(), stopPropagation() and stopImmediatePropagation() make no difference. I was particularly hopeful that stopImmediatePropagation() would work since it prevents other listeners on the same object from being invoked. It seems that listeners are first-on, first-called.

So next up, I try to remove the listener. This presents something of a conundrum. Listeners can only be removed by name or reference in Dart. Unfortunately, this in Dart does not refer to the current function as it does in Javascript. Thus the following will not work:
  // This will not work!!!
  ws.
    on.
    message.
    add((event) {
      print("The data in the event is: " + event.data);
      completer.complete(JSON.parse(event.data));

      event.target.on.message.remove(this);
    });
If I am really clever, I can use partial application to generate a named function that can be added to the listener list:
_mkCompleterListener(Completer completer) {
  var listener;
  listener = (event) {
    print("The data in the event is: " + event.data);
    completer.complete(JSON.parse(event.data));

    event.target.on.message.remove(listener);
  };
  return listener;
}

wsSync(method, model) {
  final completer = new Completer();
  // ...
  // Handle messages from the server, completing the completer
  ws.
    on.
    message.
    add(_mkCompleterListener(completer));

  return completer.future;
}
That actually works:


The _mkCompleterListener() creates a closure around the current completer. This allows the completer to be completed properly with the response value from the websocket, which then gets sent in back to the calling collection or model for action. The value of listener is also a closure, which is assigned to the anonymous function that will be returned by _mkCompleterListener(). Declaring the variable before assigning it gives the anonymous function access to it. Something like var listener = (event) { /* ... */} will not work because listener is not defined before the anonymous function is compiled, resulting in a no-such-variable exception.

Regardless, this works, but it feels overly complex. Indeed the simplest solution that I eventually come up with is to simply name the function inside add():
  ws.
    on.
    message.
    add(_wsHandler(event) {
      print("The data in the event is: " + event.data);
      completer.complete(JSON.parse(event.data));

      event.target.on.message.remove(_wsHandler);
    });
Instead of adding an anonymous event listener, I add the not-so-anonymous _wsHandler(). Since _wsHandler is named, I can simply remove it from the list of listeners once the websocket message has been handled properly.

Sheesh. I wish I had thought of that before trying that partial application thing. Crazy.


Day #327

No comments:

Post a Comment