Shopify/batman — Issue #220: [Patch] Avoid View rendering race condition
Author: pvande
Created: 2011-11-08 23:04:42+00:00
State: open

Multiple Views rendering into the same `yield` stand a fair chance of racing with one another to substitute into the DOM, which can leave the DOM and the Batman models in inconsistent states. The supplied patch is an attempt to reconcile these races as quickly as possible.

Additional optimizations may be needed (e.g. around destroying old bindings associated with a stale Renderer?).

``` diff
diff --git a/lib/batman.coffee b/lib/batman.coffee
index 5760a80..666f375 100644
--- a/lib/batman.coffee
+++ b/lib/batman.coffee
@@ -1875,11 +1875,20 @@ class Batman.Controller extends Batman.Object
options.view = new (Batman.currentApp?[helpers.camelize("#{controllerName}_#{@_currentAction}_view")] || Batman.View)(options)

if view = options.view
+ yieldInto = options.into || 'main'
+ Batman.DOM._yieldViews[yieldInto]?.stop()
+ Batman.DOM._yieldViews[yieldInto] = view
+
Batman.currentApp?.prevent 'ready'
+
+ view.on 'stopped', ->
+ Batman.currentApp?.allow 'ready'
+
view.on 'ready', =>
Batman.DOM.replace (options.into || 'main'), view.get('node')
Batman.currentApp?.allowAndFire 'ready'
view.ready?(@params)
+
view

# Models
@@ -2981,6 +2990,9 @@ class Batman.View extends Batman.Object
# Fires once a node is parsed.
@::event('ready').oneShot = true

+ # Fires when the Renderer is stopped.
+ @::event('stopped').oneShot = true
+
# Where to look for views on the server
prefix: 'views'

@@ -3003,6 +3015,8 @@ class Batman.View extends Batman.Object
return @node
set: (_, node) -> @node = node

+ stop: ->
+ @_renderer?.stop() unless @_renderer?.event('rendered')._oneShotFired

render: (node) ->
@event('ready').resetOneShot()
@@ -3014,6 +3028,10 @@ class Batman.View extends Batman.Object
# We use a renderer with the continuation style rendering engine to not
# block user interaction for too long during the render.
if node
+ if typeof @contentFor is 'string'
+ Batman.DOM._yieldViews[@contentFor]?.stop()
+ Batman.DOM._yieldViews[@contentFor] = this
+
@_renderer = new Batman.Renderer(node, =>
yieldTo = @contentFor
if typeof yieldTo is 'string'
@@ -3030,6 +3048,7 @@ class Batman.View extends Batman.Object
, @contexts)

@_renderer.on 'rendered', => @fire('ready', node)
+ @_renderer.on 'stopped', => @fire('stopped')

# DOM Helpers
# -----------
@@ -3319,6 +3338,7 @@ Batman.DOM = {
true
}
_yieldContents: {} # name/content pairs of content to be yielded
+ _yieldViews: {} # name/View pairs of View objects for given yields
_yields: {} # name/container pairs of yielding nodes

# `Batman.DOM.attrReaders` contains all the DOM directives which take an argument in their name, in the
--
```