Maintaining state in a Lisp web app

Table of Contents

1 Introduction

Consider the following situation: A user visits your web app and enters some information. They then click a few links, visit a few other pages, and finally land on a page that needs the information they entered at the beginning. How can we store this information and retrieve it later when it's needed? This note shows one such possibility.

2 Example code

Here is the source code of state.lisp:

(require "hunchentoot")
(require "cl-who")
(use-package :cl-who)
(use-package :hunchentoot)

(defclass search-server (acceptor)
  ((dispatch-table
    :initform '()
    :accessor dispatch-table
    :documentation "List of dispatch functions")
   (ss-counter
    :initform 0
    :accessor ss-counter
    :documentation "Counter for anonymous dispatch")))

(defvar *mysrv* (make-instance 'search-server :port 80))

(defmethod gendisp ((srv search-server)) ;;called by anon-once
  (concatenate 'string
               "/AD-"
               (write-to-string (incf (ss-counter srv)))))

(defun find-not-nil (l p) ;;called by acceptor-dispatch-request
  (if (endp l)
      nil
    (or (funcall p (car l))
        (find-not-nil (cdr l) p))))

(defmethod acceptor-dispatch-request ((srv search-server) (req request))
  (let ((l (find-not-nil (dispatch-table srv)
                         (lambda (disp) (funcall disp req)))))
    (or l (call-next-method))))

(defmacro anon-static (&body body) ;;called by search-form
  `(anon-once (,(gensym)) ,@body))

(defmacro anon-once ((var) &body body) ;;called by search-form and anon-static
  `(let ((auri (gendisp *acceptor*)))
     (push-dispatcher srv
                      (my-prefix-disp auri (lambda (,var) ,@body)))
     auri))

(defmacro with-html ((var) &body body) ;;caled by dummy-dispatch
  `(with-html-output-to-string (,var)
                               ,@body))

(defmacro just-html (&body body) ;;called by search-form
  `(with-html-output-to-string (,(gensym))
                               ,@body))

(defun my-prefix-disp (prefix handler) ;;called by anon-once
  (lambda (req)
    (let ((m (mismatch prefix (script-name* req))))
      (if (or (null m) (>= m (length prefix)))
          (funcall handler req)))))

(defun push-dispatcher (srv disp) ;;called by anon-once
  (push disp (dispatch-table srv)))

(defun search-form (srv) ;;called by dummy-dispatch
  (just-html
   (:p
    (:form :name "form" :method :post
           :action (anon-once (req)
                              (just-html
                               (:html
                                (:body "Great. Follow this link for more info!  "
                                       (:a :href (anon-static
                                                  (just-html
                                                   (:html
                                                    (:body "Welcome "
                                                           (str (parameter "name" req)))))) "Click" )))))
           "Query: "
           (:input :type :text :id "name" :name "name")
           (:input :type "submit")))))


(let ((counter 0))
  (defun dummy-dispatch (req)
    (with-html (s)
               (:html
                (:body
                 "Request Number: "
                 (str (incf counter))
                 (:br)
                 (str (search-form *acceptor*)))))))


(push-dispatcher *mysrv*
                 (my-prefix-disp "/info" (quote dummy-dispatch)))

(start *mysrv*)

Start the web app as follows:

$ sbcl
* (load "state.lisp")

You may see a few warnings; these can be ignored.

3 Testing it out

Launch a web browser anmd visit http://yoursite/info. If everything is working, you'll see something like

screenshot_state_1.png

Enter a name in the textbox:

screenshot_state_2.png

Submit the form:

screenshot_state_3.png

Click the link:

screenshot_state_4.png

Note that we could carry out this process in multiple tabs, and each tab would maintain an independent name for the user.

Created: 2020-08-04 Tue 07:46

Validate