375 lines
13 KiB
Plaintext
375 lines
13 KiB
Plaintext
cont-responder v0.3
|
|
===================
|
|
|
|
NOTE: This documentation is slightly out of date with respect to the
|
|
current code but contains the basic idea.
|
|
|
|
In a continuation based web application the current position within
|
|
the web application is identified by a random URL. In this example the
|
|
URL is generated as a string of random digits:
|
|
|
|
: get-random-id ( -- id )
|
|
#! Generate a random id to use for continuation URL's
|
|
<% 16 [ random-digit % ] times %> ;
|
|
|
|
Each URL is associated with a quotation or continuation. When that URL
|
|
is accessed, that quotation is executed. The quotation will receive an
|
|
alist on the top of the stack which holds any POST or query
|
|
parameters. A global table is maintained that holds the association of
|
|
quotations to id's.
|
|
|
|
: continuation-table ( -- <namespace> )
|
|
#! Return the global table of continuations
|
|
"cont" get ;
|
|
|
|
: reset-continuation-table ( -- )
|
|
#! Create the initial global table
|
|
<namespace> "cont" set ;
|
|
|
|
: register-continuation ( quot -- id )
|
|
#! Store a continuation in the table and associate it with
|
|
#! a random id.
|
|
continuation-table [ get-random-id dup [ set ] dip ] bind ;
|
|
|
|
: get-registered-continuation ( id -- cont )
|
|
#! Return the continuation associated with the given id.
|
|
continuation-table [ get ] bind ;
|
|
|
|
: resume-continuation ( value id -- )
|
|
#! Call the continuation associated with the given id,
|
|
#! with 'value' on the top of the stack.
|
|
get-registered-continuation call ;
|
|
|
|
When a an URL is accessed, the continuation for the specific URL is
|
|
obtained and called. That continuation needs to exit back to the
|
|
caller when it has some HTML that it needs to display. returning that
|
|
HTML to the caller. To exit back to the caller it calls an 'exit
|
|
continuation' that is stored in an "exit" variable:
|
|
|
|
: exit-continuation ( -- exit )
|
|
#! Get the current exit continuation
|
|
"exit" get ;
|
|
|
|
: call-exit-continuation ( value -- )
|
|
#! Call the exit continuation, passing it the given value on the
|
|
#! top of the stack.
|
|
"exit" get call ;
|
|
|
|
: with-exit-continuation ( quot -- )
|
|
#! Call the quotation with the variable "exit" bound such that when
|
|
#! the exit continuation is called, computation will resume from the
|
|
#! end of this 'with-exit-continuation' call, with the value passed
|
|
#! to the exit continuation on the top of the stack.
|
|
[ "exit" set call call-exit-continuation ] callcc1 nip ;
|
|
|
|
All this calling of continuations is hidden behind a single 'show'
|
|
call. 'show' will take a quotation on the stack. That quotation should
|
|
return an HTML string. 'show' will call it to generate the HTML and
|
|
call the exit continuation with this string on the stack so it gets
|
|
returned to the httpd responder. The quotation receives a 'url' on the
|
|
top of the stack which is the 'id' of the continuation to resume when
|
|
that URL is accessed.
|
|
|
|
The HTML page that 'show' displays can contain links to
|
|
'callbacks'. These are links to other quotations that when called
|
|
will perform some action and return back to the calling page. To
|
|
return back to the calling page 'show' must capture and store the
|
|
current continuation before 'show' does anything so it can be later
|
|
return to by the callback. The following words store the current
|
|
continuation and retrieve it:
|
|
|
|
: store-callback-cc ( -- )
|
|
#! Store the current continuation in the variable 'callback-cc'
|
|
#! so it can be returned to later by callbacks. Note that it
|
|
#! recalls itself when the continuation is called to ensure that
|
|
#! it resets it's value back to the most recent show call.
|
|
[
|
|
[ "callback-cc" set call ] callcc0 drop store-callback-cc
|
|
] callcc0 ;
|
|
|
|
To generate the string of HTML I use 'with-string-stream' which calls
|
|
a quotation and all output inside that call gets appended to a string:
|
|
|
|
: with-string-stream ( quot -- string )
|
|
#! Call the quotation with standard output bound to a string output
|
|
#! stream. Return the string on exit.
|
|
<namespace> [
|
|
"stdio" <string-output-stream> put call "stdio" get stream>str
|
|
] bind ;
|
|
|
|
: show ( quot -- alist )
|
|
#! Call the quotation with the URL associated with the current
|
|
#! continuation. Return the HTML string generated by that code
|
|
#! to the exit continuation. When the URL is later referenced then
|
|
#! computation will resume from this 'show' call with a alist on
|
|
#! the stack containing any query or post parameters.
|
|
store-callback-cc
|
|
[
|
|
register-continuation swap with-string-stream
|
|
call-exit-continuation
|
|
] callcc1
|
|
nip ;
|
|
|
|
An httpd get responder is used that takes the ID as an argument, retrieves
|
|
the continuation associated with it and calls it. For the post
|
|
responder the post data is converted into an alist and it is put on
|
|
the top of the stack when calling the continuation.
|
|
|
|
: cont-get-responder ( id -- )
|
|
#! httpd responder that retrieves a continuation and calls it.
|
|
[ f swap resume-continuation ] with-exit-continuation
|
|
serving-html print drop ;
|
|
|
|
: post-request>alist ( post-request -- alist )
|
|
#! Return an alist containing name/value pairs from the
|
|
#! post data.
|
|
dup "&" swap str-contains [
|
|
"(.+)&(.+)" groups [ "(.+)=(.+)" groups uncons car cons ] inject
|
|
] [
|
|
"(.+)=(.+)" groups uncons car cons unit
|
|
] ifte ;
|
|
|
|
: cont-post-responder ( id -- )
|
|
#! httpd responder that retrieves a continuation for the given
|
|
#! id and calls it with the POST data as an alist on the top
|
|
#! of the stack.
|
|
[
|
|
read-post-request post-request>alist swap resume-continuation
|
|
] with-exit-continuation
|
|
serving-html print drop ;
|
|
|
|
Some code to install the responder:
|
|
|
|
: install-cont-responder ( -- )
|
|
#! Install the cont-responder in the table of httpd responders
|
|
"httpd-responders" get [
|
|
<responder> [
|
|
[ cont-get-responder ] "get" set
|
|
[ cont-post-responder ] "post" set
|
|
reset-continuation-table
|
|
] extend "cont" set
|
|
] bind ;
|
|
|
|
Now to test it. Here's a function that displays some text on an HTML page:
|
|
|
|
: display-page ( title -- )
|
|
#! Display a page with some text to test the cont-responder.
|
|
#! The page has a link to the 'next' continuation.
|
|
[
|
|
swap [
|
|
"<a href='" write write "'>Next</a>" write
|
|
] html-document
|
|
] show drop ;
|
|
|
|
Note that it contains an A HREF link to the URL that resumes the
|
|
computation (The quotation passed to show has this URL passed to it by
|
|
show).
|
|
|
|
An example of a POST request is a page that requests input of a
|
|
name. The following function calls show to display it and returns the
|
|
result of the 'name' field by retrieving it from the alist. Notice how
|
|
the 'action' of the post request is set to the URL passed in to the
|
|
quotation passed to show. So the 'next' that happens here is not an A
|
|
HREF but the action field of the POST.
|
|
|
|
: display-get-name-page ( -- name )
|
|
#! Display a page prompting for input of a name and return that name.
|
|
[
|
|
"Enter your name" [
|
|
"<form method='post' action='" write write "'>" write
|
|
"Name: <input type='text' name='name' size='20'>" write
|
|
"<input type='submit' value='Ok'>" write
|
|
"</form>" write
|
|
] html-document
|
|
] show
|
|
"name" swap assoc ;
|
|
|
|
A word that displays a sequence of these pages would be:
|
|
|
|
: test-cont-responder ( alist - )
|
|
#! Test the cont-responder responder by displaying a few pages in a row.
|
|
drop
|
|
"Page one" display-page
|
|
"Hello " display-get-name-page cat2 display-page
|
|
"Page three" display-page ;
|
|
|
|
This displays the first page, then a page prompting for the
|
|
name. "Hello " is concatenated to the result of the page and a third
|
|
page is displayed. A fourth page is available as well.
|
|
|
|
The following registers this word with the continuation system:
|
|
|
|
: register-test-cont-responder ( -- id )
|
|
#! Register the test-cont-responder word so that accessing the
|
|
#! URL with the returned ID will call it.
|
|
"httpd-responders" get [
|
|
"cont" get [
|
|
[ test-cont-responder ] register-continuation
|
|
] bind
|
|
] bind ;
|
|
|
|
This returns an ID which can be used from the web server to resume
|
|
it. Start the web server:
|
|
|
|
8888 httpd
|
|
|
|
Now access the URL with that id:
|
|
|
|
http://localhost:8888/cont/1234567890
|
|
|
|
(replace 1234567890 with the ID returned by register-continuation above)
|
|
|
|
You'll see the first page and a link. Click the link and you'll see
|
|
the second page requesting a name. Enter the name and press 'Ok'. A
|
|
third page will display a message using that name. You can book mark,
|
|
refresh, go back, enter a new name, etc as expected.
|
|
|
|
You can do any form of computation inside the handlers. Here's an
|
|
example of looping a set number of times:
|
|
|
|
: test-cont-responder2 ( alist - )
|
|
#! Test the cont-responder responder by displaying a few pages in a loop.
|
|
[ "one" "two" "three" "four" ] [ display-page ] each
|
|
"Done!" display-page ;
|
|
|
|
: register-test-cont-responder2 ( -- id )
|
|
#! Register the test-cont-responder2 word so that accessing the
|
|
#! URL with the returned ID will call it.
|
|
"httpd-responders" get [
|
|
"cont" get [
|
|
[ test-cont-responder2 ] register-continuation
|
|
] bind
|
|
] bind ;
|
|
|
|
There is currently a limited ability to do 'callbacks'. You can
|
|
register a quotation as an HTML A HREF anchor thar when accessed by
|
|
the browser will run the quotation and then return to the most recent
|
|
'show' call. This has the effect of allowing 'subroutine' calls as
|
|
page links that can do anything (including display other pages and
|
|
complicated action) and will return back to the originating page. The
|
|
word to generate this link is:
|
|
|
|
: quot-href ( text quot -- )
|
|
#! Write to standard output an HTML HREF where the href,
|
|
#! when referenced, will call the quotation and then return
|
|
#! back to the most recent 'show' call (via the callback-cc).
|
|
#! The text of the link will be the 'text' argument on the
|
|
#! stack.
|
|
"<a href='" write
|
|
"callback-cc" get [ call ] cons append register-continuation write
|
|
"'>" write
|
|
write
|
|
"</a>" write ;
|
|
|
|
An example of use is a simple menu page that displays links to the
|
|
words written previously:
|
|
|
|
: test-cont-responder3 ( alist - )
|
|
#! Test the quot-href word by displaying a menu of the current
|
|
#! test words. Note that we drop the 'url' argument to the show
|
|
#! quotation as we don't link to a 'next' page.
|
|
drop
|
|
[
|
|
drop
|
|
"Menu" [
|
|
"<ol>" write
|
|
"<li>" write
|
|
"Test responder1" [ test-cont-responder ] quot-href
|
|
"</li>" write
|
|
"<li>" write
|
|
"Test responder2" [ test-cont-responder2 ] quot-href
|
|
"</li>" write
|
|
"</ol>" write
|
|
] html-document
|
|
] show drop ;
|
|
|
|
: register-test-cont-responder3 ( -- id )
|
|
#! Register the test-cont-responder3 word so that accessing the
|
|
#! URL with the returned ID will call it.
|
|
"httpd-responders" get [
|
|
"cont" get [
|
|
[ test-cont-responder3 ] register-continuation
|
|
] bind
|
|
] bind ;
|
|
|
|
You should now be able to click on the menu items, navigate through
|
|
those pages and when the sequence of pages ends, return back to the
|
|
original menu page.
|
|
|
|
Note that this is just a proof of concept. In a real framework the
|
|
continuations need to be expired after time. It would also enable
|
|
generating links to other pages, etc rather than just a sequence of
|
|
pages. I plan to flesh this out over the next few days and present
|
|
some more useful examples. My main point was to see if it was possible
|
|
to do this type of thing in Factor.
|
|
|
|
The number of words required to get things going is amazingly small
|
|
and it was very easy to develop this far.
|
|
|
|
The code is contained in cont-responder.factor. Here are the steps to
|
|
run all the examples starting with a default Factor 0.60 install:
|
|
|
|
1) java -cp Factor.jar factor.FactorInterpreter
|
|
-db:factor.db.FileStore:factor.db
|
|
|
|
At the prompt enter:
|
|
|
|
---8<---
|
|
USE: httpd-responder
|
|
default-responders
|
|
exit
|
|
---8<---
|
|
|
|
2) java -cp Factor.jar factor.FactorInterpreter
|
|
-db:factor.db.FileStore:factor.db
|
|
-no-compile
|
|
|
|
At the prompt enter:
|
|
|
|
---8<---
|
|
USE: httpd
|
|
"cont-responder.factor" run-file
|
|
USE: cont-responder
|
|
init-cont-responder
|
|
register-test-cont-responder .
|
|
---8<---
|
|
|
|
Make note of the number returned by the last line call this [1].
|
|
|
|
---8<---
|
|
register-test-cont-responder2 .
|
|
---8<---
|
|
|
|
Make note of the number returned by the last line call this [2].
|
|
|
|
---8<---
|
|
register-test-cont-responder3 .
|
|
---8<---
|
|
|
|
Make note of the number returned by the last line call this [3].
|
|
|
|
---8<---
|
|
8888 httpd
|
|
---8<---
|
|
|
|
For the first example go to http://localhost:8888/cont/[1]
|
|
where you replace [1] with the number from [1] above. In my system
|
|
it was:
|
|
|
|
http://localhost:8888/cont/0406763029866672
|
|
|
|
For the second example go to http://localhost:8888/cont/[2]
|
|
where you replace [2] with the number from [2] above. In my system
|
|
it was:
|
|
|
|
http://localhost:8888/cont/6018237533813007
|
|
|
|
For the menu example go to http://localhost:8888/cont/[3]
|
|
where you replace [3] with the number from [3] above. In my system
|
|
it was:
|
|
|
|
http://localhost:8888/cont/3568874223456634
|
|
|
|
3) You can use the inspector to look at the continuation table:
|
|
|
|
http://localhost:8888/inspect/global'httpd-responders'cont'cont |