Lift Weekly Recap 5: Comets and funcNames oh my!
Promise I’m trying to catch up! Since our last recap, a second winter storm of doom hit the Atlanta area, and a variety of other things have conspired to keep me otherwise busy or holed up in an apartment with nothing to keep me company but a cat and a sore throat. It’s been 18 days since the last recap, making this weekly recap a not-so-weekly one, sadly… Fortunately we still haven’t quite caught up to today in our historical retrospective through changes that have made it into Lift 3, so we’ll pretend it’s all okay and continue with some very exciting stuff!
As an ongoing rule, if you have any ideas for things that might be mentioned in something like this, drop me a line at savedfastcool AT gmail.com . In the meantime, before we get into our continuing coverage of new things in Lift 3, let’s have a look at one of the two changes that landed in Lift 2.6+3 the last couple of weeks.
Customizable function name generation #
One of Lift’s key features is that, when binding form fields and AJAX callbacks, they are given secure random names specific to this request and session. This is a great way to defend against various attacks including cross-site request forgery, but it leads to some difficulty in testing. Sometimes, you need to select a field by its name for a Selenium test, and Lift’s random names make that impossible.
For this purpose, when the run mode is test
and the user hasn’t explicitly disabled the functionality (wrapping S.disableTestFuncNames
around a block that generates function names), generated names are stable and based on the stack trace of your code. Until now, the function that generated these stable names (as well as the one that generated the random names) was not user-customizable; it was instead buried in the S
singleton.
In PR 1506, japgolly contributed a slight rework of this function generation logic that made it customizable. There is now a LiftRules. funcNameGenerator
, which is a zero-argument function that returns a String
to be used as the name of a function. It is called each time a new function name is needed. The default implementation of funcNameGenerator
has the same behavior as has been used in Lift until now: in non-test mode, we use a secure random number prefixed by an F
, while in test mode we use a padded hash of the first two non-Lift/Scala entries in the stack where the generator was called, prefixed by an f
.
Worth mentioning: often the best way to hook up test lookups is using CSS with semantic class names attached to the appropriate inputs. But, if you can’t do that for any reason and you’ve been running into issues with unstable names in your tests, have a look at this new Lift rule!
CometActor
of mine #
Ah, CometActor
s. The bread and butter of Lift’s real-time support. Things have gotten a little more customizable in comet-land in Lift 3! Let’s have a look at some delta management improvements that landed to support Lift 3’s real-time streaming promises.
Delta plan #
When you pass a JsCmd
to a partialUpdate
call in CometActor
s, that JsCmd
is added to an internal collection of commands that haven’t yet been sent to the client, along with a version and a timestamp of when it was generated. When the client initiates a request, it includes the version of the last delta it saw, and the commands since then are sent down to the client. Keep in mind one CometActor
can serve multiple client pages, so this lets different pages track their last-seen versions separately. However, this also means we can’t discard a delta once we’ve sent it down. To avoid accumulating too many deltas in memory, deltas older than 2 minutes have always been discarded by Lift. In Lift 3, the mechanism used to expire deltas is now exposed so it can be overridden.
The key to changing the delta pruning algorithm is to send the CometActor
a SetDeltaPruner
message with the appropriate pruning function. The pruning function takes the LiftCometActor
whose deltas should be pruned and the list of Delta
instances (which track the timestamp
of the deltas and the js
that they contain, as well as a version stamp called when
), and returns a pruned list of deltas.
Here’s an example of setting the pruning function to keep deltas around for 5 minutes:
class MyComet extends CometActor {
def localSetup = {
this ! SetDeltaPruner(fiveMinutePruner _)
}
// other comet stuff
private def fiveMinutePruner(actor: LiftCometActor, deltas: List[Delta]) = {
val currentTime = Helpers.millis
deltas.filter { delta =>
(currentTime - delta.timestamp) < 5 * 60 * 1000)
}
}
}
But, Lift 3 actually gives us a little bit more information to work with as well. LiftCometActor
now exposes a property (well, a method, but backed by a private instance variable) called lastListenerTime
. This property is the last version that we saw in a request from a client. Having this means we can make some more interesting delta pruners, as well. For example:
class MyComet extends CometActor {
def localSetup = {
this ! SetDeltaPruner(lastSeenDeltaPruner _)
}
// other comet stuff
private def lastSeenDeltaPruner(actor: LiftCometActor, deltas: List[Delta]) = {
val lastSeenVersion = actor.lastListenerTime
deltas.filter(_.when > lastSeenVersion)
}
}
This pruner throws away all deltas that have already been seen by a client. Now, this only makes sense if you know you’ll only ever have one client for this comet, but there are actually a decent number of people using comet actors this way. This lets you throw deltas as soon as your single client receives the commands. Under the covers, Lift 3 does exactly this with streaming promises, because streaming promise actors are unique per page.
This recap’s pretty long, so rather than lengthening it I’ll leave the next big Lift 3 addition for next time: custom data-
attributes and element handling for snippets! I’ll also outline a performance improvement to lift-json that made it into both 2.6 and 3 in the last couple of weeks.