Overview ======== The 'cont-responder' library is a continuation based web server for writing web applications in Factor. Each 'web application' is a standard Factor httpd responder. This document outlines how to write simple web applications using 'cont-responder' by showing examples. It does not attempt to go into the technical details of continuation based web applications or how it is implemented in Factor. Instead it uses a series of examples that can be immediately tried at the Factor prompt to get a feel for how things work. Getting Started =============== To get started you will first need to use the 'cont-responder' vocabulary: USE: cont-responder The responders that you will be writing will require an instance of the httpd server to be running. It will be run in a background thread to enable the interactive development of the applications. The following is a simple function to start the server on port 8888: USE: httpd USE: threads : start-httpd [ 8888 httpd ] in-thread ; start-httpd Responders ========== A 'responder' is a word that is registered with the httpd server that gets run when the client accesses a particular URL. When run that word has 'standard output' bound in such a way that all output goes to the clients web browser. In the 'cont-responder' system there are two words used to set output to go to the web browser and display a page. They are 'show' and 'show-final'. Think of them as 'show a page to the client'. 'show' and 'show-final' both take a single item on the stack and that is a 'page generation' quotation. A 'page generation' quotation is a quotation which when called will output HTML to stdout. In the httpd system, stdout is bound to the socket connection to the clients web browser. The 'page generation' quotation passed to 'show' should have stack effect ( string -- ) while that for 'show-final' has stack effect ( -- ). The two words have very similar uses. The big difference is with 'show'. It provides an URL to the page generation quotation that when requested will proceed with execution immediately following the 'show', and any POST request data will be on the stack. With 'show-final' no URL is passed so it is not possible to 'resume' computation. This is explained more fully later. Hello World 1 ============= A simple 'hello world' responder would be: : hello-world1 ( -- ) [ "
"This is a paragraph" write
=>This is a paragraph
You can write open and close tags like orginary HTML and anything sent to standard output in between the tags will be enclosed in the specified tags. Attributes can also be used:"More text" write
=>More text
The attribute must be seperated from the value of that attribute via whitespace. If you are using attributes the tag must be closed with a '[tagname]>' where the [tagname] is the name of the tag used. See the '' example above. You can use any factor code at any point: "text-align: " "red"
"Using style " write swap write write
=>Using style text-align: red
Tags that are not normally closed are written using XML style closed tag (ie. with a trailing slash): "One" writetag so it is formatted correctly. : memory-stats1 ( -- ) ["Memory Statistics" write
"Total Data Memory" write | room unparse write |
"Free Data Memory" write | unparse write |
"Total Code Memory" write | unparse write |
"Free Code Memory" write | unparse write |
room.] show-final ; "memorystats1" [ memory-stats1 ] install-cont-responder Accessing this page will show a table with the current memory statistics. Hitting refresh will update the page with the latest information. The HTML output can be refactored into different words. For example: : memory-stats-table ( free total -- ) #! Output a table containing the given statistics.
"Total Data Memory" write | unparse write |
"Free Data Memory" write | unparse write |
"Page 1" write
] show drop ["Page 2" write
] show drop ["Page 3" write
] show-final ; "flowexample1" [ flow-example1 ] install-cont-responder The 'flow-example1' word contains two 'show' calls in a row, followed by a 'show-final'. The 'show' calls display simple pages with an anchor link to the URL received on the stack. This URL when accessed resumes the computation. The final page doesn't require resumption of the computation so 'show-final' is used. We could have used 'show' and dropped the URL passed to the quotation and the result following the 'show' but using 'show-final' is more efficient. When you display this example in the browser you'll be able to click the URL to navigate. You can use the back button to retry the URL's, you can clone the browser window and navigate them independantly, etc. The similarity of the functions above shows that some refactoring would be useful. The pages are almost exactly the same so we seperate this into a seperate word: : show-flow-page ( n bool -- ) #! Show a page in the flow, using 'n' as the page number #! to display. If 'bool' is true display a link to the #! next page. [ ( n bool url -- )"Page " write rot unparse write
swap [ ] [ drop ] ifte ] show 3drop ; : flow-example2 ( n -- ) #! Display the given number of pages in a row. dup 1 - [ dup 1 + t show-flow-page ] repeat f show-flow-page ; "flowexample2" [ 5 flow-example2 ] install-cont-responder In this example the 'show-flow-page' pulls the page number off the stack. It also gets whether or not to display the link to the next page. Notice that after the show that a '3drop' is done whereas previously we've only done a single 'drop'. This is due to a side effect of 'show' using continuations. After the 'show' call returns there will be one item on the stack (which we've been dropping and will explain later what it is). The stack will also be set as it was before the show call. So in this case the 'n' and 'bool' remain on the stack even though they were removed during the page generation quotation. This is because we resumed the continuation which, when captured, had those items on the stack. The general rule of thumb is you will need to account for items on the stack before the show call. This example also demonstrates using the 'repeat' combinator to sequence the page shows. Any Factor code can be called and the continuation based system will sequentially display each page. The back button, browser window cloning, etc will all continue to work. You'll notice the URL's in the browser have an 'id' query parameter with a sequence of characters as its value. This is the 'continuation identifier' which is like a session id except that it identifies not just the data you have stored but your location within the responder as well. Forms and POST data =================== The web pages we've generated so far don't accept input from the user. I've mentioned previously that 'show' returns a value on the stack and we've been dropping it in our examples. The value returned is a namespace containing the field names and values of any POST data in the request. If no POST data exists then it is the boolean value 'f'. To process input from the user just put a form in the HTML with a method of 'POST' and an action set to the URL passed in to the page generation quotation. The show call will then return a namespace containing this data. Here is a simple example: : accept-users-name ( -- name ) #! Display an HTML requesting the users name. Push #! the name the user input on the stack.. [accept-users-name write ", Good to see you!" write
] show-final ; "post-example1" [ post-example1 ] install-cont-responder The 'accept-users-name' word displays an HTML form allowing input of the name. When that form is submitted the namespace containing the data is returned by 'show'. We bind to it and retrieve the 'username' field. The name used here should be the same name used when creating the field in the HTML. 'post-example1' then does something a bit tricky. Instead of first calling 'accept-users-name' to push the name on the stack and then displaying the resulting page we call 'accept-users-name' from within the page itself when we actually need it. The magic of the continuation system causes the 'accept-users-name' to be called when needed displaying that page first. It is certainly possible to do it the other way though: : post-example2 ( -- ) accept-users-name [ ( name url -- )write ", Good to see you!" write
] show-final ; "post-example2" [ post-example2 ] install-cont-responder Associating URL's with words ============================ A web page can contain URL's that when clicked perform some action. This may be to display other pages, or to affect some server state. The 'cont-responder' system enables an URL to be associated with any Factor quotation. This quotation will be run when the URL is clicked. When that quotation exits control is returned to the page that contained the call. The word that enables this is 'quot-href'. It takes two items on the stack. One is the text to display for the link. The other is the quotation to run when the link is clicked. This quotation should have stack effect ( -- ). This example displays a number which can be incremented or decremented. 0 "counter" set : counter-example1 ( - ) #! Display a global counter which can be incremented or decremented #! using anchors. ["++" [ "counter" get 1 + "counter" set ] quot-href "--" [ "counter" get 1 - "counter" set ] quot-href
] show-final ; "counter-example1" [ counter-example1 ] install-cont-responder Accessing this example from the web browser will display a count of zero. Clicking '++' or '--' will increment or decrement the count respectively. This is done by calling a quotation that either increments or decrements the count when the URL's are clicked. Because the count is 'global' in this example, if you clone the browser window with the count set to a specific value and increment it, and then refresh the original browser window you will see the most recent incremented state. This gives you 'shopping cart' like state whereby using the back button or cloning windows shows a view of a single global value that can be modified by all browser instances. ie. The state is not backtracked when the back button is used. You'll notice that when you visit the root URL for the responder that the count is reset back to zero. This is because when the responder was installed the value of zero was in the namespace stack. This stack is copied when the responder is installed resulting in initial accesses to the URL having the starting value. This gives you 'server side session data' for free. Local State =========== You can also have a counter value with 'local' state. That is, cloning the browser window will give you a new independant state value that can be incremented. Going to the original browser window and refreshing will show the original value which can be incremented or decremented seperately from that value in the cloned window. With this type of state, using the back button results in 'backtracking' the state value. A way to get 'local' state is to store values on the stack itself rather than a namespace: : counter-example2 ( count - ) [ ( count URL -- )"++" over [ 1 + counter-example2 ] cons quot-href "--" swap [ 1 - counter-example2 ] cons quot-href
] show-final ; "counter-example2" [ 0 counter-example2 ] install-cont-responder This example works by taking the value of the counter and consing it to a code quotation that will increment or decrement it then call the responder again. So if the counter value is '5' the two 'quot-href' calls become the equivalent of: "++" [ 5 1 + counter-example2 ] cons quot-href "--" [ 5 1 - counter-example2 ] cons quot-href Because it calls itself with the new count value the state is remembered for that page only. This means that each page has an independant count value. You can clone or use the back button and all browser windows have an independant value. Calling 'Subroutines' ===================== Being able to call other page display functions from 'quot-href' gives you subroutine like functionality in your web pages. A simple menu that displays a sequence of pages and returns back to the main page is very easy: : show-page ( n -- ) #! Show a page in the flow, using 'n' as the page number #! to display. [ ( n url -- )"Page " write swap unparse write
] show 2drop ; : show-some-pages ( n -- ) #! Display the given number of pages in a row. [ dup 1 + show-page ] repeat ; : subroutine-example1 ( -- ) ["Please select:" write
Please select:
We can continue to drill down using 'test-cont-click' using the URL's above to see the HTML for each 'click'. Here's an example using post data. We'll test the 'post-example1' word written previously:Chris, Good to see you!
As you can see the post data was sent correctly.