BorisMoore / jsviews

Interactive data-driven views, MVVM and MVP, built on top of JsRender templates
http://www.jsviews.com/#jsviews
MIT License
856 stars 130 forks source link

jsviews observable array remove seems to "hang" my script/websocket handler #416

Closed nonchip closed 5 years ago

nonchip commented 5 years ago

I got a (coffee)script that continuously gets updates to a structure via websockets and syncs those to an jsviews observable array by matching id members inside them, using the function:

  obsupdate = (o,n)-> # o is the array, n is the new data
    for v,i in o # loop to remove things from o that aren't in n
      found = false
      for t in n
        if t.id == v.id
          found = true
          break
      if not found
        console.log 'remove',i
        $.observable(o).remove(i)
    for v,i in n # loop to update or add things to o that are new/different in n
      break if n.editable or o.editable
      found = false
      for t in o
        if t.id == v.id
          found = t
          break
      if not found # it's new, add it
        console.log 'insert',v
        $.observable(o).insert v
      else # it's not new
        for prop,val of v # check for changes
          if found[prop]!=val # something changed, update it
            console.log 'set',prop
            $.observable(found).setProperty prop, val

the first thing my websocket listener does, before even invoking it or touching any related data, is debuglog the received message:

  data = {
    roll_results: []
    chars: []
  }

  ws.listen (msg)->
    console.log 'received msg:', msg
    obsupdate data.chars, msg.data.chars

problem now is, as soon as i hit the "remove" case (both adding and changing stuff works perfectly fine) it removes the item as expected, but then stops receiving any websocket events, meaning i can still see the events roll in in the network debug tab and reloading the page "unsticks" it (so it's not a server issue), but the listener doesn't show any more debug output and no functions are invoked from it, as if my script was somehow just stuck/paused/busylooping. though profiling it i don't see any such thing happen.

could it be the remove function (which then through jquery removes some event handlers, presumably for the removed UI elements attached to it) just somehow messes with my (also jquery based) simpleWebSocket handler?

it even still SENDS data to the server via the exact same socket (and as mentioned, i see the server reply to it in the network debug thing), it just refuses to handle the responses. e.g. i can modify other data, which is then reflected correctly in the websocket updates, and everyone else watching the same data (who has already reloaded to unstick their page) sees it. but the local (non-reloaded) version refuses to see any of that.

BorisMoore commented 5 years ago

You probably need to debug the jQuery off() calls or jQuery.event.remove() calls that remove handlers, and see if the handler that is being removed is the simpleWebSocket handler. If so, see where that call is coming from. jQuery automatically removes event handler attached to HTML elements that are being removed from the DOM, but I don't know if simpleWebSocket is associated with any HTML/UI, or whether the listener is a jQuery handler on a data array or similar. No obvious reason why jsviews/jsobservable would be removing that handler, but you'll need to investigate further....

Incidentally you might find it useful to use compiled view models and the merge() feature (see also here and here) to replace your code above - and maintain all the data-link bindings. In your approach above you may be replacing objects without re-binding all the child objects and properties (in the 'object hierarchy'), which could also be causing some of your issues...

nonchip commented 5 years ago

You probably need to debug the jQuery off() calls or jQuery.event.remove() calls that remove handlers, and see if the handler that is being removed is the simpleWebSocket handler.

i guessed so, but i'm afraid i have no idea how to do that right now, but going to research a bit, maybe i'll figure it out.

I don't know if simpleWebSocket is associated with any HTML/UI

it shouldn't be, at least i don't see a reason for the library to do so, and my instance of it is created like this:

$ ->

  loc = window.location

  ws = $.simpleWebSocket {
    url: "ws" + (if loc.protocol == "https:" then "s" else "") + "://" + loc.host + "/ws"
  }

Incidentally you might find it useful to use compiled view models and the merge() feature

i don't know what exactly you mean by "compiled view models" (i am using the <script type="text/x-jsrender" /> style templating language essentially just with the data object as the template's "environment").

EDIT: ok if i'm reading this right the view model is an intermediate object to store the template's data instead of linking a plain object to a render, seems smart, since i already more or less do that with my data object (syncing it from the websocket message etc), i can as well use the "right way" instead.

the merge function seems to do exactly what i want to achieve, gonna try that right now before i do any debugging on my own reinvention of the wheel, thanks :)

nonchip commented 5 years ago

ok rewrote my stuff to use the view models:

 vmChar = $.views.viewModels {
    id: 'id'
    getters: [
      'name'
      'alpha'
      'beta'
      'gamma'
      'delta'
      {getter: 'editable', defaultVal: false}
    ]
  }
  chars = vmChar.map []

  ws.listen (msg)->
    console.log 'received msg:', msg
    if msg.gamedata and msg.gamedata.chars
      console.log 'char'
      chars.merge msg.gamedata.chars

  $'#chars'.html $.templates("#tpl_char").render chars, helpers

now it "freezes" as soon as the first websocket message got handled. also i have no idea how to implement the "don't update this element if we're editing it right now" logic using the new system. but i can take care of that part after i figured out the websocket, guess i'll try another library for now, simpleWebSocket doesn't seem to be maintained that well :/

nonchip commented 5 years ago

ok that definitely was an issue with simpleWebSocket, even just a simple new Websocket().onmessage works fine. only remaining problem is for some reason when doing the switch to the view model i replaced the link call with a render call, which obviously broke the datalinking, but after fixing that it works perfectly, thanks for your help :)

nonchip commented 5 years ago

whoops, that was a bit quick to close, it's actually not fully working, you see, i'm using logic like this:

<input type='checkbox' data-link='editable()' title="edit">
{^{if editable()}}
  <input data-link="somevalue()">
{{else}}
  <span data-link="text{:somevalue()}"></span>
{{/if}}

but now clicking the checkbox doesn't do anything anymore after the switch to the new model

EDIT: my mistake, i had a template error because i forgot to define an unrelated getter that was used in the same line. now i just have to figure out how to prevent merge from changing anything that locally is set editable=true...

tried this ugly hack:

  vmChar =  $.views.viewModels {.....}
  vmcm = vmChar.prototype.merge
  vmChar.prototype.merge = (data)->
    if this._editable
      return
    return vmcm.call(this,data)

in the hopes the prototype would propagate correctly but it doesn't seem to have any effect. also tried without the .prototype but that also didn't affect anything.

if i apply the hack to the instance instead of the model it also doesn't seem to do anything, probably because it just doesn't propagate through the array (the instance is a vmChar.map []).

nonchip commented 5 years ago

my solution now is really ugly ~but works~, by overriding every single property getter:

  protectEditable = (name)->
    getter=(value)->
      if !arguments.length
        return this['_'+name]
      if this._editable
        return
      $.observable(this).setProperty('_'+name, value)
    getter.set=(value)->
      this['_'+name]=value
    return getter

  vmChar = $.views.viewModels {
    id: 'id'
    getters: [
      'name'
      'alpha'
      'beta'
      'gamma'
      'delta'
      'mod'
      {getter: 'editable', defaultVal: false}
    ]
    extend: {
      name: protectEditable('name')
      alpha: protectEditable('alpha')
      beta: protectEditable('beta')
      gamma: protectEditable('gamma')
      delta: protectEditable('delta')
      editable: protectEditable('editable')
    }
  }
nonchip commented 5 years ago

aaaaaaaand screwed up again. the underscore in $.observable(this).setProperty('_'+name, value) was just preventing it from show how it actually got overwritten during the edit.

i have no idea what to do now /o\

nonchip commented 5 years ago

nevermind, it all works now, removing the underscore was the right thing, i just forgot to add an unmap call in my websocket sending function so the server never saw the changes and reverted them on the next update.

BorisMoore commented 5 years ago

I'm glad it worked for you. Can you point me to the simpleWebSocket repo that you are using?

I wondered whether it might make sense to include a sample on jsviews.com of using web sockets, and the ViewModel approach with merge. If you are interested in creating a sample that we could publish there, let me know.

For the server piece it could if appropriate use the https://github.com/BorisMoore/jsrender-node-starter project. We could augment that project to provide the backend, and publish to heroku, for example.

If that idea makes sense/interests you, you could send me your suggested code (for a sample) and I could take a look... I could also look at the scenario you are addressing of "don't update this element if we're editing it right now", if it needs better support in the ViewModel merge feature...

nonchip commented 5 years ago

Can you point me to the simpleWebSocket repo that you are using?

well i'm using plain websocket objects now because the simpleWebSocket repo broke.

nonchip commented 5 years ago

in case you still want to see the code, I released the tool there: https://gitlab.com/nonchip/lite-roller.nonchip.de

specifically important i guess would be the main coffee file: https://gitlab.com/nonchip/lite-roller.nonchip.de/blob/master/static/main.coffee and the server code (which is moonscript running in openresty's ngx.lua module): https://gitlab.com/nonchip/lite-roller.nonchip.de/blob/master/apps/socket.moon

BorisMoore commented 5 years ago

Thanks. I took a look. Of course you are using a few languages which I have never used myself, like lua, moonscript.... Glad to see you using a full range of JsViews features, and that you are able to integrate that well into your environment. What is the nature of the resulting app/site. Is it deployed?

For creating a sample, it would need to be stripped down to something minimalist, using only javascript (with nodejs on the server), so it may not straightforward to use your code as starting point for doing that... But thanks for showing me your code... Maybe in the future I'll look at doing a sample, when I have got up to speed on websockets etc.

BTW congratulations on your excellent English!

nonchip commented 5 years ago

it is a tool for synchronized "dice" (d2 aka coins) rolling and result tracking for a pen&paper RPG, and yes it is deployed (at the same fqdn as the project's name), but not (yet?) able to support more than one campaign of users at once, so please don't mess up our game :P

i'm using the websocket to broadcast (actually poll, since i wanted to avoid RPC hell between server threads, but hey one round trip per second over websocket is way better still than ajax spamming) the state represented by the jsviews data so everyone sees/edits the same thing essentially.

if you want to deploy it yourself locally to play around with it:

BorisMoore commented 5 years ago

Thanks for giving me that background. It gives me a better sense of what you are doing...

I won't have time for a while, but at some point I may look more closely at it, and play with it locally... :)