40ants / reblocks-websocket

Websocket support for Reblocks framework
https://40ants.com/reblocks-websocket/
5 stars 2 forks source link

Demo fails: `REBLOCKS/PAGE::*CURRENT-PAGE*` is unbound #4

Open hraban opened 2 months ago

hraban commented 2 months ago

I have create a small demo based off of the tutorial:

(defpackage test
  (:use #:cl
        #:arrow-macros
        #:reblocks-ui/form
        #:reblocks/html)
  (:import-from #:reblocks/widget
                #:render
                #:update
                #:defwidget)
  (:import-from #:reblocks/actions
                #:make-js-action)
  (:import-from #:reblocks/app-actions
                #:define-action)
  (:import-from #:reblocks/app
                #:defapp)
  (:import-from #:reblocks-websocket
                #:websocket-widget)
  (:export #:main))

(in-package #:test)

(defapp page :prefix "/")

(defwidget counter-box (reblocks-websocket:websocket-widget)
  ((counter :initform 0
            :accessor counter)))

(defmethod initialize-instance ((instance counter-box) &rest restargs)
  (declare (ignorable restargs))
  (call-next-method)

  (reblocks-websocket:in-thread ("Update counter")
    (sleep 3)
    ;; Updating counter
    (incf (counter instance))
    (update instance)))

(defmethod reblocks/page:init-page ((app page) (url-path string) expire-at)
  (declare (ignorable app url-path expire-at))
  (make-instance 'counter-box))

(defmethod render ((instance counter-box))
  (with-html
    (:h1 "Counter: " (counter instance))
    (with-html-form (:POST (lambda (&key action &allow-other-keys)
                             (cond
                               ((equal action "+")
                                (incf (counter instance)))
                               ((equal action "-")
                                (decf (counter instance))))
                             (update instance)))
      (:input :type :submit :name "action" :value "+")
      (:input :type :submit :name "action" :value "-"))))

(defun main ()
  (reblocks/server:start :port 4000))

But on first page load I get this error:

The variable REBLOCKS/PAGE::*CURRENT-PAGE* is unbound.
   [Condition of type UNBOUND-VARIABLE]

Restarts:
 0: [CONTINUE] Retry using REBLOCKS/PAGE::*CURRENT-PAGE*.
 1: [USE-VALUE] Use specified value.
 2: [STORE-VALUE] Set specified value and use it.
 3: [ABORT] Abort request processing and return 500.
 4: [RESET-SESSION] Reset current Weblocks session and return 500.
 5: [ABORT] abort thread (#<THREAD tid=13315 "hunchentoot-worker-127.0.0.1:61318" RUNNING {700F0146F3}>)

Backtrace:
  0: ((:METHOD INITIALIZE-INSTANCE (TEST::COUNTER-BOX)) #<TEST::COUNTER-BOX {700FF5DF13}>) [fast-method]
  1: (SB-PCL::FAST-MAKE-INSTANCE #<unavailable argument> #<unavailable &MORE argument>)
  2: ((:METHOD REBLOCKS/PAGE:INIT-PAGE :AROUND (T STRING T)) #<TEST::PAGE {700877F113}> "/websocket" NIL) [fast-method]
  3: (REBLOCKS/PAGE::CALL-WITH-PAGE-DEFAULTS #<FUNCTION (LAMBDA NIL :IN REBLOCKS/REQUEST-HANDLER::HANDLE-NORMAL-REQUEST) {700FF1B2EB}>)
  4: ((LAMBDA (#:G43 &REST #:G44) :IN REBLOCKS/REQUEST-HANDLER:HANDLE-REQUEST) NIL)
  5: ((:METHOD REBLOCKS/REQUEST-HANDLER:HANDLE-REQUEST (REBLOCKS/APP:APP)) #<TEST::PAGE {700877F113}>) [fast-method]
  6: ((FLET "doit-46" :IN REBLOCKS/REQUEST-HANDLER:HANDLE-REQUEST))
  7: ((:METHOD REBLOCKS/REQUEST-HANDLER:HANDLE-REQUEST :AROUND (T)) #<TEST::PAGE {700877F113}>) [fast-method]
  8: (LOG4CL-EXTRAS/ERROR::CALL-WITH-LOG-UNHANDLED #<FUNCTION (LAMBDA NIL :IN REBLOCKS/REQUEST-HANDLER:HANDLE-REQUEST) {700FA5E66B}> :DEPTH NIL :ERRORS-TO-IGNORE NIL)
  9: ((LAMBDA NIL :IN REBLOCKS/REQUEST-HANDLER:HANDLE-REQUEST))
 10: (REBLOCKS/ERROR-HANDLER::CALL-WITH-HANDLED-ERRORS #<FUNCTION (LAMBDA NIL :IN REBLOCKS/REQUEST-HANDLER:HANDLE-REQUEST) {700FA5E4DB}>)
 11: ((FLET REBLOCKS/RESPONSE::WITH-RESPONSE-THUNK :IN REBLOCKS/SERVER::HANDLE-HTTP-REQUEST))
 12: (REBLOCKS/RESPONSE::CALL-WITH-RESPONSE #<FUNCTION (FLET REBLOCKS/RESPONSE::WITH-RESPONSE-THUNK :IN REBLOCKS/SERVER::HANDLE-HTTP-REQUEST) {700FA5E2FB}>)
 13: ((LAMBDA NIL :IN REBLOCKS/SERVER::HANDLE-HTTP-REQUEST))
 14: (REBLOCKS/APP::CALL-IN-WEBAPP #<TEST::PAGE {700877F113}> #<FUNCTION (LAMBDA NIL :IN REBLOCKS/SERVER::HANDLE-HTTP-REQUEST) {700FA5D82B}>)
 15: ((FLET REBLOCKS/HOOKS:CALL-NEXT-HOOK :IN REBLOCKS/COMMANDS-HOOK::RESET-COMMANDS-LIST))
 16: ((FLET REBLOCKS/COMMANDS-HOOK::RESET-COMMANDS-LIST :IN "/nix/store/iaddlq8j9c8p9aqvz5i0rfyf4ij3mk4s-system-reblocks/src/commands-hook.lisp") (#<FUNCTION (LAMBDA (#:G23 &REST #:G24) :IN REBLOCKS/SERVER..
 17: ((FLET REBLOCKS/DEBUG::TRACK-LATEST-SESSION :IN REBLOCKS/DEBUG:ON) (#<FUNCTION (FLET REBLOCKS/COMMANDS-HOOK::RESET-COMMANDS-LIST :IN "/nix/store/iaddlq8j9c8p9aqvz5i0rfyf4ij3mk4s-system-reblocks/src/co..
 18: ((LAMBDA NIL :IN REBLOCKS/SERVER::HANDLE-HTTP-REQUEST))
 19: ((FLET "WITHOUT-INTERRUPTS-BODY-" :IN SB-THREAD::CALL-WITH-RECURSIVE-LOCK))
 --more--

I had a look in the guts of reblocks-websocket and it seems quite fundamental; you need the page to send updates, but this initializer is building that very same page. How do you break that cycle?¯

svetlyak40wt commented 2 months ago

I've never tried to execute IN-THREAD during the page initialization. Previously I did this inside the RENDER method.

However, if you are calling update on counter-box widget, it will cause creation of a new thread each time. To prevent this, wrap your in-thread form with (unless reblocks-websocket:*background* (in-thread ...)).

hraban commented 2 months ago

Ah, I was going off of the tutorial in the documentation here https://40ants.com/reblocks-websocket/ which has:

(defmethod initialize-instance ((instance counter-box) &rest restargs)
  (declare (ignorable restargs))
  (call-next-method)

  (reblocks-websocket:in-thread ("Update counter")
    (sleep 3)
    ;; Updating counter
    (incf (counter instance))
    (reblocks:update instance)))

I'm still not quite clear on where else to put the code. Everywhere I put it leads to either errors, deadlocks, concurrent hash access violations, etc. Do you maybe have a live example somewhere?

svetlyak40wt commented 2 months ago

Try to move it to the RENDER method.

hraban commented 2 months ago

Great thanks, this is definitely an improvement.

~These also get triggered for async POSTs though--I guess that makes sense, but is there an easy way to only run code on the initial actual payload and not on async requests?~ found it.

Feel free to use this to update the docs for this package!

Current code:

(defpackage test
  (:use #:cl
        #:reblocks-ui/form
        #:reblocks/html)
  (:import-from #:reblocks-parenscript)
  (:import-from #:reblocks/widget
                #:render
                #:update
                #:defwidget)
  (:import-from #:reblocks/app
                #:defapp)
  (:import-from #:reblocks-websocket
                #:websocket-widget)
  (:export #:main))

(in-package #:test)

(defapp page :prefix "/")

(defwidget counter-box (reblocks-websocket:websocket-widget)
  ((counter :initform 0
            :accessor counter)))

(defmethod reblocks/page:init-page ((app page) (url-path string) expire-at)
  (declare (ignorable app url-path expire-at))
  (make-instance 'counter-box))

(defmethod render ((instance counter-box))
  (unless (or reblocks-websocket:*background* (reblocks/request:ajax-request-p))
    (reblocks-websocket:in-thread ("Update counter")
      (sleep 3)
      ;; Updating counter
      (incf (counter instance))
      (update instance)))
  (with-html
    (:h1 "Counter: " (counter instance))
    (with-html-form (:POST (lambda (&key btn &allow-other-keys)
                             (cond
                               ((equal btn "+")
                                (incf (counter instance)))
                               ((equal btn "-")
                                (decf (counter instance))))
                             (update instance)))
      (:input :type :submit :name "btn" :value "+")
      (:input :type :submit :name "btn" :value "-"))))

(defun main ()
  (reblocks/server:start :port 4000))