Maintaining state in a Lisp web app

Table of Contents


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.

Example code

Here is the source code of stateexample.lisp:

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

(defclass search-server (acceptor)
    :initform '()
    :accessor dispatch-table
    :documentation "List of dispatch functions")
    :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
               (write-to-string (incf (ss-counter srv)))))

(defun find-not-nil (l p) ;;called by acceptor-dispatch-request
  (if (endp l)
    (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)))

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

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

(defun file-get-contents (filename)
  (with-open-file (stream filename)
                  (let ((contents (make-string (file-length stream))))
                    (read-sequence contents stream)

(defun my-prefix-disp (prefix handler) ;;called by anon-once
  ;; Input #1: Prefix (i.e. part of a url like "/bob")
  ;; Input #2: handler (name of a function that takes a request object and returns html)
  ;; This returns a function that checks the request and then calls the handler if the request
  ;; matches the prefix (it only has to match the prefix - /bob will match /bobbie)
  (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)))

(load "info.lisp")

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

(start *mysrv*)

You'll see it references another file, info.lisp

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

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

Start the web app as follows:

$ sbcl
* (load "state.lisp")

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

Testing it out

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


Enter a name in the textbox:


Submit the form:


Click the link:


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

Date: 2020-08-04 Tue 00:00