| Author: | Robin Bryce |
|---|---|
| Requires: | MochiKit.Base, MochiKit.Async |
| Organization: | Wiretooth Ltd |
| Copyright: | (c) 2005 Robin Bryce. All rights reserved. This program is dual-licensed free software; you can redistribute it and/or modify it under the terms of the MIT License, or the Academic Free License v2.1 |
Abstract
Implements a bi directional event stream between browser and backend server. Based on the design proposed on the MochiKit list on 2005/11/06. Current design has some small but significant changes, See Dispatcher for latest thinking.
The desired end, is a bi-directional circuit ,pigy backing on a polling http/s request, that delivers events sourced at either the client or the backend.
An HttpRequest is wrapped up in a Protocol abstraction such that it can be conveniently invoked at regular intervals, by the Dispatcher to pass events back and forth between the client js and the backend server. Coupling between the Dispatcher and the Protocol implementation is kept to an absoloute minimum. Example implementation: XMLHttpRequestProtocol(servicepath,dispatchparametername,names,values,sdpp)
Latency is always going to be a trade off beteen server load, client performance, and responsiveness. Its possible that complex interactions between the client and the backend, possibly in conjuntion with allowing the backend to hold on to polls, could help but such things are well beyond the scope of this implementation.
Each time the request is triggered it gets a JSON formatted URL parameter containing the pending client queries and any pending client responses to backend originated queries. The format of both query and response items is: [id,opaquedata]. Query and response messages are packaged up into two distinct JSON arrays:
[[clientqueries],[responses to server queries]]
The server is expected to respond with JSON formated data in a similar, but complimentary layout:
[[serverqueries][responses to client queries]]
Note: URL standards allow for multi-valued parameters. At the server side, dispatcherurlparameter[0] is the set of queries and responses from the client:
urlparameter[0] =>[[clientqueries],[responses to server queries]]
In the client-side, the responses to backend originated events, and new client-side events, pile up in Dispatch until interval seconds after the last request completes. Both the client-side and the server side are allowed to take more than one http request/response cycle to redeem a request. Each side of the circuit (client or backend) is responsible for numbering the event messages it originates.
Dispatcher.query(body,d) is how the client sends queries to the backend. The body parameter is treated as opaquedata, but note that it will be passed through a JSON serialization step. Internaly, the dispatcher generates an id and queues a query message for the next request cycle. Optionaly, the deferred for catching the reply may be passed in, the d parameter, and a new one will be created if it is not. The returned deferred will be fired later when the backend responds with a response_id whose value matches the id the client side dispatcher assigned to the query. The client never sees this id. It just cares about the deferred. The 'result' when this deferred fires, is the backends servers answer to the clients query.
Dispatcher.respond(response_id,body) is how the client responds to queries origniating from the backend server. It takes whatever id the backend chose to give the event message. body is the clients answer to the backends query.
At regular intervals, frequency determined by the client, Dispatcher.poll() is called. It returns a deferred, on to which Dispatcher.dispatch_result(result) has been chained. dispatch_result fires all the client query deferreds that are satisfied by server response. Unless the client has setup an observer, all queries originating at the backend are siltently ignored. dispatcher_testilities.js Has an example observer implementation.
Dispatcher.start_polling(observer,defaultinterval) is how the client should initiate polling when the backend is an event source as well as an event sink. Having initiated polling in this way, Dispatcher._continue_polling(result) will collect backend queries from each poll, and, invoke observer.handle_queryitems. It is leagal for the observer to replace itself, and or adjust the polling interval in this method. But see Dispatcher._continue_polling(result) before doing this.
The client side should use Dispatcher.respond(response_id,body) to reply to any queries collected in this way. It is the job of the observer to make the id allocated by the backend, for its query, available to the client as response_id.
The client side mostly just cares about deferreds and the format of the body data. The message 'container' format and numbering is delt with by the combination of Dispatch, Protocol. The only place the client needs to take care with id's in in its observer implementation.
Event messages are allways in json format. Arguably this need not be the case and the desicion rests with the Protocol implementation.
backends/twistedweb.py provides example backends. Run it up then
- http://localhost:8080/examples/dispatch/docs/dispatch.html is this document.
- http://localhost:8080/examples/dispatch/test_dispatch.html runs the unit tests for dispatch.js
url preperation for protocol objects.
Implements initialization of an Dispatcher compatible protcol object without commiting to a particular kind of request object. It can also be used as a convenience method for preparing parameters for XMLHttpRequest, but do NOT pass in an instance of XMLHttpRequest as self !
Take care when supplying additional query parameters. If one of your parameter names collides with dispatchparametername, then the first element of the array of values for that parameter is clobbered by the dispatch data.
If you host your backend on the same domain as the client script the parameter sdpp should be left undefined or set to null.
sdpp only comes into play if you need to script accross domains.
sdpp defaults to using the [scheme,domain,port,path] of the currentDocument and the path part is replaced with servicepath. ie., By default, avoid security issues attendant to cross domain scripting.
Couple of handy references for this issue: mozilla signed-scripts.html and apple xmlhttpreq.html
Note, in particular two urls, differing only in the port number, ie:
http://www.mywebhost.com/index.html http://www.mywebhost.com:50000/mybackend.json
Are on different domains as far as the definition of 'cross-domain' is concered. If index.html pulls in java script that references the latter, then that script is crossing domains ...
AFAICT mozilla(Firefox et all), do not support anything like macromedia crossdomain.xml, and arguably shouldn't !
self: victim object
If self is null then the this object is used. If typeof(self) is "undefined" a new object is created. Otherwise self is used as is.
servicepath: path part of the url of the backend
dispatchparametername: url dispatch query parameter name.
Defaults to "dispatch".
names,values: extra query parameters
As per queryString. But note if names contains dispatchparametername it's associated value is replaced with the dispatch data.
sdpp: array of [scheme, domain, port|null, path|null]
Defaults to split_schemedomainandport(currentDocument().URL) path part is always replaced with servicepath
rtype: self (or whatever it became after coercion from null or undefined)
Standard polling protocol.
Implements a Dispatcher compatible dispatch protcol using MochiKit.Async.getXMLHttpRequest as its basis. See prepare_protcol for a detailed explanation of the other parameters.
servicepath: path of the backend resource representing the other end of the circuit.
dispatchparametername,names,values,sdpp: See prepare_protocol(self,servicepath,dispatchparametername,names,values,sdpp)
Polls the backend using XMLHttpRequest
Calls initialize
Initialize a dispatcher.
Send a query to the backend.
Returns a deferred that will be signaled with the result.
body: The body of the query.
d: The deferred to signal when the result is available. If null or undefined one will be created for you.
rtype: Deferred
respond to a query initiated by the backend.
response_id: the id allocated by the backend for its query.
body: the body of your response.
Internal method,
Gets chained onto the delegate returned by Dispatcher.poll to consume and dispatch backend responses to client originated queries.
Returns an iterable containing any queries that were intiated by the backend in time for the last poll.
result: The protcol is expected to process the response data into this form:
[iterablequeries, iterableresponses]
=> [
[[queryid1,data1], ... [queryidN,dataN]],
[[responseid1,data1], ... [responseidN,dataN]]
]
For this method, the perspective for interpreting queryid's and responseids is looking from the client towards backend.
Each item in result[0], iterablequeries, is then an asynchronous query from the backend to the client. Similarly, each item in result[1], iterableresponses, is a response from the backend to a query that originated in the client.
This function processes the backend responses and fires the client side delegates that are pending those responses. It returns the backend originated queries for the next callable in the dispatch chain.
rtype: iterablequeries, these are the new queries from the backend to the client. Note: queries with null or "undefined" tags indicate, the backend does not want or expect a response to that query.
Poll the backend exactly once.
start_polling causes this to be called at regular intervals. If you need to force a poll, call stop_polling first.
rtype: Deferred
Set the dispatcher polling.
defaultinterval: The default polling interval If there is no observer or it doesn't specify one, then use this value as the delay between the last result and the next poll.
note: If you need to be clever about retry intervals etc then the observer is the place to do that.
Internal method
call the observer with the backend queries like: observer.handle_queryitems(queryitems, dispatcher)
Note: the call to observer.handle_queryitems precedes this functions check of this.polling and the interval. This is to allow the observer to imediately terminate. the observer is trusted to call stop_polling from within observer.handle_queryitems rather than simply asigning to dispatcher.polling, which would/could break stuff.
It is perfectly resonable for the observer to replace itself with an alternate during observer.handle_queryitems. ie, some handler would do:
dispatcher.stop_polling(); dispatcher.start_polling(otherobserver);
Note: While it is legal for the observer to modify dispatcher.polling_interval in handle_queryitems, setting it to null or "undefined" is not.
Split a url into its constituent parts.
If port and or path parts are missing from the url, the associated fields are set to null:
sdpp => [scheme, domain, port|null, path|null]
Both, join_schemedomainandport and replace_schemedomainandport_path cope with this. Note that:
join(split('http://some.domain')) => 'http://some.domain/'
join(split('http://some.domain:8080')) => 'http://some.domain:8080/'
join(split('http://some.domain/')) => 'http://some.domain/'
join(split('http://some.domain:8080/')) => 'http://some.domain:8080/'
Ie., root path is forced if it is missing.
url: url to split. if null or undefined currentDocument().URL is used.
Compliment of split_schemedomainandport
sdpp: [scheme, domain, port|null, path| null]
Like, join but replacement clobbers path.
This is a shortcut for:
var url = split_schemedomainandport(url); url[3] = replacement; url = join_schemedomainandport(url);
Serialize queued queries.
Into a format appropriate for a GET/POST request parameter. If you need to escape or otherwise encode data elements, see join_json_encodeitems
queryitems: [[queryid1,data1], ... [queryidN,dataN]]
responseitems: [[responseid1,data1], ... [responseidN,dataN]]
rtype: json formated string
join, with custom item encoding.
See join_json
queryitemencoder: called for each datafield in queryitems, should return encode(queryitemdata).
responseitemencoder: called for each datafield in responseitems, should return encode(queryitemdata).
If repsonseitemencoder is null or undefined it defaults to queryitemencoder.
If both responseitemencoder and queryitemencoder are either null or undefined this function short circuits to join_json.
Converse of join_json(queryitems, responseitems).
WARNING: uses eval, You need to decide if you trust data.
data: a json string, assumed to describe an array with the form
[iterablequeries, iterableresponses][[[queryid1,data1], ... [queryidN,dataN]],[[responseid1,data1], ... [responseidN,dataN]]]
rtype: Iter
don't think it makes sense to provide 'split_json_decodeitems' as it would be highly sensitive to different join_json_encodeitems encoding schemes. If you can't use end to end json, or, more importantly, you decide not to trust eval, then you need to write a custom split for your protocol implementation.
Loopback implementation for Protocol.poll.
Provided for the benefit of test code. Echos query items, encoded in clientdata, as the loopbackresult.
clientdata: the serialized and encoded queries & responses to submit on the clients behalf. response items in clientdata are silently ignored.
latency: how long the poll should take. the default is 0. Note that latency <= 0 is special, it forces synchronous behaviour.
emulatedbackendpollresult: emulated backend responses, Serialized and encoded queries & responses fake response to this poll. By default, response items are not included in the items looped back to the client.
ignoreemulatedresponses: Don't throw away emulated responses. if this parameter is true then emulated backend responses are included in the loopback result.
if latency was zero or unspecified the returned deferred has allready been fired.:
if latency unspecified or latency <= 0
returns succede(loopbackresult)
Otherwise:
wait(latency, loopbackresult)
rtype: Deferred