Notes on using comet with Seaside

I’ve been using the Seaside web development framework for a project at school for the past few weeks and overall have found it to be quite enjoyable. Its amazing how quickly I was able to get a fairly complex web application up and running, especially considering all of the advanced JavaScript that I wanted to include in the project. I read the Seaside book to learn how to integrate the “pushiness” of comet into my project and hit a bit of a rocky road when I tried to take the simple examples in the book as well as in the Comet-Examples package that comes with Seaside and try to adopt them for use in my application.

Issue #1: Page never appears to finish loading

I was able to get basic comet up and running rather quickly, but I noticed when I loaded the page, the browser would continue to appear as if the page was constantly loading. This was because of the way that the Seaside book instructs you to add the comet script to your page. The book example code looks like:

html script: (html comet
     pusher: self class pusher;
     connect)

Assuming:

MyComponent class>>#pusher
     ^pusher ifNil: [ pusher := CTPusher new ]

What this actually does is place a block of JavaScript code in the generated HTML that the browser immediately executes in the foreground when it encounters it. Due to the nature of the comet implementation that Seaside provides (streaming), the browser immediately opens up an asynchronous connection to the Seaside server and streams from it, stopping the browser from doing anything else. This is why it appears to constantly be loading. The proper way to add comet to a Seaside application is as follows:

html document
    addLoadScript: (html comet
    pusher: self class pusher;
    connect)

This causes the browser to start up the comet streaming connection when the page has finished loading, and in the background. Thus the page will appear to the end user as if it is completely loaded, even though the streaming comet connection is open.

Issue #2: Using comet with a custom session

I still haven’t solved this error yet, but hopefully I will soon, so while I wait to hear back from the Seaside mailing list, I’ll describe the kludge I put in to make it work right.

For my application, I needed a custom subclass of WASession to hold on to information about the logged in user. Unfortunately, when I used this session class with comet, things broke. On the first request of a new session, I would constantly get stacktraces like:

Comet Error Stacktrace With Custom Session

It appears as if the parent of my custom session was never being set to the WAApplication instance that it should have been. It only happened on the first request of the session, and all subsequent comet interactions worked as expected. I thought I could fix this problem by overriding initialize in my custom session, but that did not work:

initialize
    ^super initialize

So, I posted my question to the Seaside mailing list and am still awaiting a response. In the meantime, I have added a “patch” to CTHandler>>notifySession to change it from:

notifySession
    self session application cache
        notifyRetrieved: self session
        key: self session key

to:

notifySession
    [self session application cache
        notifyRetrieved: self session
        key: self session key]
    on: MessageNotUnderstood do: [:error | ]

This basically attempts to run the old notifySession code, but when the MessageNotUnderstood error is raised, it simply ignores it and continues on with execution. I know this is not the best practice, but it does make the application work as expected.

Issue #3: Duplicated JavaScript code sent to clients

I didn’t notice this bug until I used FireBug and took a look at the JS that Seaside was generating from my Smalltalk code and streaming to the clients. What I found out, much to my surprise, was that every single JavaScript action that I had coded once in SmallTalk was being sent over the wire twice, both identical. This took a bit to trace down, but again it had to do with inadequate examples from the Seaside book. The Seaside book makes use of jQuery for all of its work with comet (as do the comet examples that come with Seaside itself), and I had chosen to use Prototype/Scriptaculous for my application, so this is where the issue might have arisen.

Say you wanted to push some JavaScript that would set the contents of the div element with id=’peanut’ to be ‘butter’. The Seaside book and examples led me to believe that using Scriptaculous/Prototype, the code should look like:

pushToClient
    self class pusher
        javascript: [ :script |
            script <<  (script scriptaculous element
                id:'peanut';
                update: 'butter').
        ].

But this actually causes the div to be updated twice in rapid succession, both to the same thing. It was completely unnoticeable on the client side, but it was extra overhead being executed on the client browser as well as extra unnecessary data being sent over the wire.

Thanks to the fantastic debugger built into Pharo, I was able to trace down the problem and rewrite the above code as follows:

pushToClient
    self class pusher
        javascript: [ :script |
            script scriptaculous element
                id:'peanut';
                update: 'butter'.
        ].

The script variable is an instance of a JSScript object, and the #<< message means to append the argument onto its list of JavaScript statements. What I found was that when using the JSScript>>scriptaculous message, it was already appending the generated JavaScript statements onto the JSScript, so the #<< message was simply redundant. Eliminating all usages of #<< in my comet code fixed the error and everything works as expected now.

Issue #4: Conditionally pushing to clients

In my application, I have regular users and administrators. They are defined as such based on what type of object is attached to the custom session I wrote for the project. There are certain elements on my pages that only appear for regular users and certain elements that only appear for administrators, as well as a large number of elements that appear in both. I wanted to use a single comet pusher to be able to push to both regular users as well as administrators, to reduce connection overhead in the final deployed version of the application. Having a separate CTPusher for regular users and a separate one for administrators would have worked, but would have been painful. I wanted something a little cleaner. So I did some hacking on the comet source that comes with seaside and this is what I came up with (added):

javascript: aBlock forHandlers: handlerBlock
	"Evaluate aBlock with an JSScript instance and pushes the resulting script-string aString to
	handlers which when evaluated as the argument to handlerBlock causes it to return true."

	| script |
	script := (JSScript context: self renderContext)
		rendererClass: self rendererClass;
		yourself.
	aBlock value: script.
	self push: (String streamContents: [ :stream |
		stream
			nextPutAll: '<script type="text/javascript">parent.Comet.eval(';
			javascript: script asJavascript;
			nextPutAll: ')</script>' ])
		forHandlers: handlerBlock

And:

push: aString forHandlers: aBlock
	"Push aString to handlers which when passed as the argument to aBlock causes
	it to return true."
	| pushHandlers |
	pushHandlers := handlers select: aBlock.
	self mutex critical: [ pushHandlers do: [ :each | each push: aString ] ]

I do realize that this does not do exactly what the plain CTHandler>>#push message does. Namely, it does not prune down the handlers list to ensure that only the connected ones continue to receive updates from the server, but in my application I do mostly regular pushes with only a few requiring the handlers block, so I feel like I am pretty much safe for now. So now, using the code I added, if I want to update the div on the page with id ‘peanut’ to contain the text ‘butter’ for regular users, but to ‘jelly’ for administrators, its quite simple:

pushToClient
    self class pusher
        javascript: [ :script |
            script scriptaculous element
                id:'peanut';
                update: 'butter'.
        ] forHandlers: [ :handler | handler session user isRegularUser ].
    self class pusher
        javascript: [ :script |
            script scriptaculous element
                id:'peanut';
                update: 'jelly'.
        ] forHandlers: [ :handler | handler session user isAdministrator ].

So those are my notes on using comet with Seaside thus far. Overall its been a lot of fun watching something really complex come together in such a short period of time. Next, when I get time, I’ll post about how I was able to get custom basic authentication working using LDAP in Seaside.

  1. No comments yet.

  1. No trackbacks yet.