ΛↃ LAMBDACOMBINE

Symbolic Systems Infrastructure

02 · Signals

REACTIVE STATE · PATCH-SIGNALS · SERVER-DRIVEN VARIABLES
Mercury Space Flight Network (MSFN) (source: NASA)
Mercury SFN (src: NASA)

Signals are reactive variables that live in the browser. They are declared with data-signals and referenced in other data-* attributes using the $ prefix. When a signal changes, every bound element updates automatically.

Declare signals with a JSON-like object. String defaults use single quotes to avoid HTML attribute escaping:

(:div :data-signals "{time: '', ticks: 0}"
      (:p :data-text "$time")
      (:p :data-text "$ticks"))

patch-signals

The server pushes new signal values via patch-signals. It accepts a plist of signal name strings to values. The browser applies a JSON Merge Patch and all bound elements update.

(d*:with-sse (gen hunchentoot:*request*)
  (let ((tick 0))
    (loop
      (d*:patch-signals gen
                        (list "time"  (get-time-string)
                              "ticks" (incf tick)))
      (sleep 1))))

This replaces the HTML-patching loop from chapter 01. The DOM update is driven by signal values, not by the server sending HTML fragments. Use signals when the same value needs to appear in multiple places, or when client-side expressions depend on it.

The Tao of Datastar cautions against overusing signals. Most application state belongs on the server. Signals are for user interaction state and for values the server needs to send back to the client.

Live Demo

Time and tick count pushed as signals. Both elements share the same SSE connection; one stream patches two independent reactive bindings.

Time: --:--:--

Ticks: 0

Full Source (standalone Hunchentoot)

;;;; -*- Mode: LISP; fill-column: 80; coding: utf-8 -*-

;;;; DEMO-SIGNALS.LISP --- Signal handling example
;;;;
;;;; Copyright (C) 2025, 2026 Frederico Muñoz / ΛↃ lambda combine
;;;;
;;;; This file is part of datastar-cl, the Common Lisp SDK for Datastar
;;;;
;;;; License: MIT

(ql:quickload '(:hunchentoot :spinneret :datastar-cl/hunchentoot))

(defpackage #:clock
  (:use #:cl #:hunchentoot)
  (:local-nicknames (:sp :spinneret) (:d* :datastar-cl)))
(in-package #:clock)

(hunchentoot:define-easy-handler (index :uri "/") ()
  (setf (hunchentoot:content-type*) "text/html")
  (sp:with-html-string
    (:doctype)
    (:html
     (:head (:script :type "module" :src (d*:datastar-url)))
     (:body :data-signals "{time: '--:--:--', ticks: 0}"
            :data-init (d*:sse-get "/sse")
            (:h1 "Datastar-CL SSE Demo")
            (:div :id "clock" :data-text "$time")
            (:p "Ticks: " (:strong :data-text "$ticks"))))))

(hunchentoot:define-easy-handler (sse-handler :uri "/sse") ()
  (let ((tick 0))
    (d*:with-sse (gen hunchentoot:*request*)
      (loop
        (multiple-value-bind (s m h) (decode-universal-time (get-universal-time))
          (d*:patch-signals gen
                            (list "time"  (format nil "~2,'0d:~2,'0d:~2,'0d" h m s)
                                  "ticks" (incf tick))))
        (sleep 1)))))

(hunchentoot:start (make-instance 'hunchentoot:easy-acceptor :port 8989))
(format t "~&Server started on http://localhost:8989~%")

(defroute guide-signals (:get :text/html))